Only this pageAll pages
Powered by GitBook
Couldn't generate the PDF for 228 pages, generation stopped at 100.
Extend with 50 more pages.
1 of 100

Main

Loading...

Bots

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Extension Apps

Loading...

Loading...

Getting Started

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Developer Tools

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Conversational Bot

Chatbots or conversational Bots, are the most common bots in the Symphony Messaging ecosystem. These types of bots run 24/7 and wait for users to initiate conversations in order to perform certain tasks. The conversations could be simple commands or even natural language queries. The bot will determine the user’s intent based on their messages and take the appropriate action.

Chatbot Workflow

1. Kick off your Workflow

As stated before, users can interact with chatbots in chatrooms or 1 to 1 chats. In order to initiate your chatbot's workflow we recommend that users @mention the bot's username in order to get the bot's attention and signal to the bot to begin its intended function. That way, chatbots can be active members of chatrooms, but eliminate noise that is outside the scope of its intended workflow.

It is common for Bots to contain multiple commands or sub workflows that it can action upon. It is best practice to list these commands in a help menu. Additionally, it is best practice for all commands to follow a "/" so that it's clear what text is meant to be processed as a command. The following illustrates these best practices:

2. Listen for Events

A core aspect of all chatbots and conversational bots is the ability to 'listen' to different types of commands and events and reply to them accordingly. The mechanism that enables Bots to listen to events in chats is the Datafeed.

The Datafeed is a real-time message and event stream that can be created using the REST API. The datafeed provides a secure stream of messages and events from all conversations that a bot is in.

In order to 'listen' and respond to user events, bots create a single datafeed and will subsequently have access to all chat activity including non-chat based events such as users being added/removed from chatrooms, external connection requests, as well as chats being created.

To learn more about the Datafeed continue here:

3. Handle Events with Custom Business Logic

The next step of your chatbot workflow is to introduce your custom business logic. Once you have access to the bot's events and messages through the Datafeed, the next step is to create dedicated event listeners. Inside these listeners is where you parse messages, fetch data from external sources, manage conversation state, and make requests to our Symphony Messaging REST API in order to reply to users directly or perform administrative functions such as creating chatrooms.

Next Steps:

You can learn more about parsing events and introducing custom business logic .

Continue on to our guide in order to learn more about our development tools and appropriate development pathway for building your first Symphony Messaging Chatbot.

Datafeed
Building a Conversational Bot
Build a Conversational Bot

Start Your Developer Journey

Welcome, Symphony Messaging Developer!

Here you will learn about all the ways to streamline work using integrated bots and apps. By leveraging Symphony Messaging's open APIs, developers can create innovative Bots and workflows, save time with strategic automations, and customize their Symphony experience by building third-party Extension Apps.

Keep reading to learn about core Symphony Messaging concepts in order to ensure a smooth development journey from your first line of code to putting your application in production.

Getting Started

It's time to get started. Navigate to one of the following guides to kick off your developer journey today!

Looking to migrate your bot from the deprecated SDKs to the latest BDKs? Please check out our migration guides to bring your bot up to date with our latest tooling.

Begin your development journey by taking an in depth look at Symphony Messaging Bots. Understand the core concepts and all the details needed to get your Bot up and running!

Want to build your first bot together with the Developer Relations team? Head over to our developer training centre and register for our online courses to get started, and it's free.

Bring customized and extensible workflows into Symphony Messaging through Extension Apps. Understand how to build, deploy, and manage your Extension App!

Enroll in our brand new Developer Certification program and obtain sandbox access for free! Register and enroll today!

In depth documentation and reference guide for the Symphony Messaging REST API:

In depth documentation and reference guide for the Symphony Messaging Extension API:

Check out our dedicated SDKs, Bot Developer Kit, Bot Generators and more to streamline your development process!

Securely embed stand-alone Symphony Messaging chat modules inside other websites and applications:

Interactive Bot

Interactive Bots are a form of Chatbot where the conversational flow is driven by interactive Elements forms. Instead of listening for plain text as the only source of data, Interactive Bots can collect data or commands through structured forms.

Elements allow bots to send messages that contain interactive forms with pre-designed text fields, dropdown menus, person selectors, buttons and more. Additionally, Elements reuse Symphony Messaging UX component libraries, enabling developers to easily create interactive bot messages that look and feel like they belong in Symphony Messaging.

To learn more about Elements navigate here:

Interactive Bot Workflow

1. Kick off your Workflow

Users can interact with chatbots in chatrooms and 1 to 1 chats. In order to initiate your Interactive Bot's workflow we recommend that users @mention the bot's username in order to get the bot's attention and signal to the bot to begin its intended function. That way, bots can be active members of chatrooms, but eliminate noise that is outside the scope of its intended workflow.

It is common for bots to contain multiple commands or sub workflows that it can action upon. It is best practice to list these commands in a help menu. Additionally, it is best practice for all commands to follow a "/" so that it's clear what text is meant to be processed as a command. The following illustrates these best practices:

2. Listen for Events

A core aspect of all Interactive Bots is the ability to 'listen' to different types of commands and events and reply to them accordingly. The mechanism that enables Symphony Messaging Bots to listen to events in chatrooms and IMs is the Datafeed.

The Datafeed is a real-time message and event stream that can be created using the REST API. The datafeed provides a secure stream of messages and events from all conversations that a bot is in.

In order to 'listen' and respond to user events, bots create a single datafeed and subsequently have access to all chatroom activity including non-chat based events such as users being added/removed from chatrooms, external connection requests, and chatrooms IMs being created.

To learn more about the Datafeed continue here:

When an end-user submits an Elements form, the bot is able to access the contents of that form through the Datafeed. The Elements form lifecycle is illustrated below:

First an Interactive Bot sends a messageML message containing an Elements form:

<messageML>
  <form id="form_id"><h4>Personal Information</h4>
    <text-field name="name_01" required="true" placeholder="Name"/>
    <text-field name="email_01" required="true" placeholder="email"/>

    <h4>Select your country</h4>
    <select name="country">
    <option value="opt1">Australia</option>
    <option value="opt2">Brazil</option>
    <option value="opt3">China</option>
    <option value="opt4">Denmark</option>
    <option value="opt5">Ecuador</option>
    <option value="opt6">France</option>
    <option value="opt7">Germany</option>
    <option value="opt8">Italy</option>
    <option value="opt9">Japan</option>
    </select>

    <h4>Choose your option</h4>            
    <radio name="example_radio" value="option_01" checked="true">Marked</radio>
    <radio name="example_radio" value="option_02">Unmarked</radio>

    <h4>Choose your option</h4>
    <checkbox name="checkbox_1" value="value01" checked="true">Checked</checkbox>
    <checkbox name="checkbox_2" value="value02">Unchecked</checkbox>

    <h4>Send a comment</h4>
    <textarea name="comment" placeholder="Add your comment here" required="true"></textarea>

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

  </form>
</messageML>

After the user fills out their information and clicks the 'Submit' button, the following payload is captured by the Datafeed and delivered to the bot:

{
    "id": "tkld79",
    "messageId": "iJXn_RBQwGmixJN94ZSfYn___oyx4HHubQ",
    "timestamp": 1594743557649,
    "type": "SYMPHONYELEMENTSACTION",
    "initiator": {
        "user": {
            "userId": 344147139494862,
            "firstName": "Reed",
            "lastName": "Feldman",
            "displayName": "Reed Feldman (SUP)",
            "email": "reed.feldman@symphony.com",
            "username": "reedUAT"
        }
    },
    "payload": {
        "symphonyElementsAction": {
            "stream": {
                "streamId": "IEj12WoWsfTkiqOBkATdUn___pFXhN9OdA",
                "streamType": "IM"
            },
            "formMessageId": "p1tZHt5dL8ZR5RPgl4TJ6X___oyx4sfxbQ",
            "formId": "form_id",
            "formValues": {
                "action": "submit_button",
                "name_01": "Reed Feldman",
                "email_01": "reed.feldman@symphony.com",
                "country": "opt1",
                "example_radio": "option_01",
                "checkbox_1": "value01",
                "comment": "My comment!"
            }
        }
    }
}

3. Handle Events with Custom Business Logic

The next step of your Interactive Bot workflow is to introduce your bot's custom business logic. Now that you have access to the bot's events, messages, and elements payloads through the Datafeed, the next step is to create dedicated event listeners. Inside these listeners is where you parse messages and datafeed payloads, fetch data from external sources, manage conversation state, and make requests to Symphony Messaging REST APIs in order to reply to users directly or performs administrative functions such as creating chatrooms.

Next Steps:

Headless Bot

Headless Bots are the simplest form of Bots. Headless Bots usually take the form of a script that is either run on a scheduled basis or triggered by an event from an external system. These scripts are usually transient and terminate after performing their assigned task, like sending an alert or daily digest message.

Headless Bot Workflows

Scheduled Workflow

Often times, Headless Bots run on a scheduled basis. Since Headless Bots do not have a conversational aspect, they do not need to create/maintain a Datafeed. As a result, Headless Bots do not need to always be running.

In practice, each time a Headless Bot kicks off its scheduled workflow, it will:

  1. Authenticate

  2. Post a message/alert in a designated chat or call any number of API endpoints

  3. Terminate its process

External Workflow

Headless Bots can also listen for webhooks or incoming events from external/third-party systems. By listening and handling different events from multiple systems, Headless Bots are able to transform Symphony Messaging into a centralized notification center for all your data.

In practice, a Headless Bot needs to expose an endpoint in order to listen and handle these webhooks or incoming events. Each time the bot receives a webhook or an incoming request, it must also:

  1. Authenticate

  2. Post a message/alert in a designated chat or call any number of API endpoints

  3. Terminate its process

Next Steps:

Building Bots on Symphony Messaging

For even more in depth look at how to Build Bots and Apps, check out our new Developer Certification program: 🎓

Building Extension Applications

Become a Certified Developer

Messaging REST API

Extension API

Developer Tools

Embedded Modules

The messageML is sent by your Bot and rendered as a form:

You can learn more about parsing events and introducing custom business logic .

Continue on to our guide in order to learn more about our development tools and appropriate development pathway for building your first interactive Bot:

Continue on to our guide. Here you will learn how to get your own Headless Bot up and running and take a closer look at the APIs used to create a simple Headless Bot workflow:

Become a Certified Developer
Migration guide from SDK to Symphony BDK for Java
Migration guide from SDK to Symphony BDK for Python
Building Bots
Developing Bots using BDK for Java
Developing Bots using BDK for Python
Implementing Workflows with WDK
Developer Certification
Extension Apps
Developer Certification
Overview of Extension API
Developer Tools
Embedded Modules
Elements Interactive Forms
Datafeed
Building an Interactive Bot
Build an Interactive Bot
Building a Headless Bot
Build a Headless Bot
Building Bots on Symphony Messaging
Building Extension Applications on Symphony Messaging
Developer Tools
REST API Reference
Extension API Reference
Embedded Modules

Getting Started

Overview of REST API

The Symphony Messaging REST API acts as a secure interface between your Symphony Messaging Bot and Symphony Messaging's components. Symphony's architecture for bots comprises three components: the Symphony Messaging Pod, the API Agent and the Key Manager.

The Symphony Messaging REST API is spread out across these components according to the type of API being called.

While Bots can call the Symphony Messaging API directly, Symphony Messaging's dedicated BDK and WDK toolkits provide language-specific API bindings that enable bot developers to easily call the API directly from code:

API Swagger files

There is one sub-folder per API collection, and each API collection is split in two swagger files: one for the supported endpoints and a second one listing only the deprecated endpoints (e.g. /pod/pod-api-public-deprecated.yaml).

Depending on your pod or agent version, a newly introduced endpoint may not yet be available to you. You can spot these new endpoints as they are tagged with the x-since attribute. For example, an endpoint flagged as x-since: 20.15 will not be present if you are still on SBE 20.14.

REST API Architecture

To learn more about how bots securely interact with Symphony Messaging's three components, continue onto the REST API Architecture guide below:

Pod API

To learn more about how bots can make authentication and administrative calls such as manage streams, manage users or facilitate cross-pod connections, continue onto the Pod API guide below:

Key Manager API

To learn more about how bots can authenticate and encrypt messages on Symphony Messaging, continue onto the Key Manager API guide below:

Agent API

To learn more about how bots can send and receive encrypted messages on Symphony Messaging, continue onto the Agent API guide below:

REST API Reference

For the full Symphony Messaging API reference continue here:

The (Java BDK)

The (Python BDK)

The API Swagger files are available .

Java Bot Developer Kit
Python Bot Developer Kit
here
REST API Architecture
Overview of Pod API
Overview of Key Manager API
Overview of Agent API

Building Bots

Building Bots on Symphony Messaging is fast, easy, and secure. Take these simple steps in order to create and deploy your Bot today!

1. Plan Your Bot

Symphony Messaging Bots can leverage the Symphony Messaging REST API to create innovative workflows and timesaving automations. Depending on your desired workflow, there are many different development avenues available for you and your development team. Understanding these different development options and different APIs available is key to creating a successful bot and positive user experience. Learn more about the different types of Symphony Messaging Bots and APIs here:

2. Leverage our Tools

Symphony has built numerous tools in order to streamline Symphony Messaging Bot Development. Our rich suite of developer tools offers centralized configuration and authentication protocols, provides out of the box API bindings, convenient error handling, datafeed management, and ensures that your Bot is built using Symphony's best practices.

Using an ultra simplified configuration and authentication set up, an intuitive message and room control mechanism, new APIs for message templating and workflow activities, developing bots has never been easier.

Learn more about how you can get started with our developer tools here:

3. Authentication

At Symphony, security is always first. In order to leverage Symphony Messaging's robust set of open APIs, your Bot must first authenticate itself. You can learn more about how to authenticate and the different types of authentication here:

4. Add Custom Business Logic

The last step is to add custom business logic to your Bot. Begin learning about the capabilities of Symphony Messaging REST APIs and bring your workflows and automations to life:

5. Become a Certified Developer

Want to take your Bot building to the next level? Symphony offers a free course and certification program for Developers wishing to improve their Symphony Messaging skills, technical knowledge, and expertise. Learn more about how you can become a Certified Developer today!

Creating a Service Account

Guide for creating a bot user in the admin portal

Please note the below steps can only be performed by a Symphony Messaging Pod Administrator as only they will have the necessary administrator privileges to access the Admin Portal. Please do not attempt this if you are not a Pod Administrator and reach out to your internal IT Helpdesk if you are unsure who your Symphony Messaging Pod Administrator is.

In order to create a bot and begin your development journey, you must first create a bot user, also known as a service account. You can only do this yourself if you are a pod administrator on your Symphony Messaging pod. Otherwise, please seek assistance from your pod administrator or your internal IT Service Desk if you are unsure who to contact.

Create a Bot User

1. Navigate to your Symphony Messaging Admin Portal for your Pod

For example,

2. Select Create an Account in the left navigation pane

3. Select the Service Account tab

4. Fill in your bot information

The bot username here has to match the username supplied by a bot configuration file exactly.

5. Fill in the RSA Public Key

6. Set roles and entitlements

Enable the required roles that your bot requires.

Enable the required entitlements that your bot requires.

7. Confirm Bot Creation

If you are attempting to use the Developer Sandbox at , this section is not relevant for you. Please register for an account at the Developer Center at and click on the request link in the welcome email to obtain your sandbox credentials.

Paste the entire contents of the public key in the Authentication section. This public key has to match the private key supplied by a bot. If you are unsure how to generate an RSA key pair, use the .

Navigate to for a full list of bot roles and privileges.

Planning Your Bot
Getting Started with BDK
Authentication
Overview of REST API
Developer Certification

Configuration

The Symphony Messaging Generator creates a basic configuration file that assumes a fully cloud-hosted Symphony Messaging pod architecture. In this scenario, the pod, key manager and agent are all hosted on the same domain e.g. develop2.symphony.com. If your pod architecture is different and you have other connectivity requirements like a network proxy, you will need to add those options to your configuration file.

If you are using the BDK for Java's Spring Starter, the BDK configuration is embedded within Spring's configuration file under the bdk section

host: develop2.symphony.com
bot:
  username: bdk-bot
  privateKey:
    path: rsa/privatekey.pem
bdk:
  host: develop2.symphony.com
  bot:
    username: bdk-bot
    privateKey:
      path: rsa/privatekey.pem

Basic Configuration Structure

Property
Description

host

Global hostname

port

Global port

scheme

https or http

context

Context path (e.g. /abc)

pod agent keyManager

Contains component details including host, port, scheme, context and proxy attributes

bot

contains bot metadata including username, privateKeyPath, certificatePath andcertificatePassword

app

contains extension app metadata including appId, privateKeyPath, certificatePath, and certificatePassword

ssl

contains trustStore and trustStore password for SSL communication

Datafeed Configuration Structure

Property

Description

version

version of the datafeed service to be used. By default, the bot will use the datafeed v2 service.

idFilePath

the path to the file which will be used to persist a created datafeed id in case the datafeed service v1 is used

retry

the specific retry configuration can be used to override the global retry configuration. If no retry configuration is defined, the global one will be used.

Retry Configuration Structure

Property

Description

maxAttempts

maximum number of retry attempts that a bot is able to make

multiplier

after each attempt, the interval between two attempts will be multiplied by this factor

initialIntervalMillis

the initial interval between two attempts

maxIntervalMillis

the limit of interval between two attempts. For example, if the current interval is 1000 ms, multiplier is 2.0 and the maxIntervalMillis is 1500 ms, then the interval for the next retry will be 1500 ms.

An example customized configuration file is seen below:

scheme: https
host: localhost.symphony.com
port: 8443

pod:
  host: dev.symphony.com
  port: 443

agent:
  context: agent

keyManager:
  host: dev-key.symphony.com
  port: 8444

sessionAuth:
  host: dev-session.symphony.com
  port: 8444

bot:
  username: bot-name
  privateKeyPath: path/to/private-key.pem
  certificatePath: /path/to/bot-certificate.p12
  certificatePassword: changeit

ssl:
  trustStorePath: /path/to/all_symphony_certs_truststore
  trustStorePassword: changeit

app:
  appId: app-id
  privateKeyPath: path/to/private-key.pem

datafeed:
  version: v2
  retry:
    maxAttempts: 6
    initialIntervalMillis: 2000
    multiplier: 1.5
    maxIntervalMillis: 10000

retry:
  maxAttempts: 6
  initialIntervalMillis: 2000
  multiplier: 1.5
  maxIntervalMillis: 10000
develop2.symphony.com
developers.symphony.com
Symphony Messaging Generator
Bot Permissions

Overview of Key Manager API

The Symphony Messaging Key Manager API's sole purpose is to authenticate a bot or API caller with the Key Manager.

If successful, the Key Manager API returns a Key Manager Token which is valid for up to two weeks. This Key Manager Token must be passed along with every subsequent Agent API request in order to encrypt/decrypt messages on the Agent server. You can read more about Authentication and Token management here:

Getting Started with BDK

The Bot Developer Kit (BDK) is the preferred tooling for Java or Python developers to get started building bots on Symphony Messaging.

Generate your Bot

First, install yeoman and the Symphony Messaging Generator. Then, create a directory for your new bot project. Finally, launch the generator.

$ npm i -g yo @finos/generator-symphony
$ mkdir my-bot && cd $_
$ yo @finos/symphony

This will prompt you with a number of questions about your bot and pod configuration. Type in your bot's information, using arrow keys to scroll and press enter to move on to the next prompt.

When prompted for Select your type of application, choose Bot Application.

 __   __     ___                 _
 \ \ / /__  / __|_  _ _ __  _ __| |_  ___ _ _ _  _
  \ V / _ \ \__ \ || | '  \| '_ \ ' \/ _ \ ' \ || |
   |_|\___/ |___/\_, |_|_|_| .__/_||_\___/_||_\_, |
                 |__/      |_|                |__/

Welcome to Symphony Generator v2.7.1
Application files will be generated in folder: /home/user/code/my-bot
______________________________________________________________________________________________________
? Enter your pod host mycompany.symphony.com
? Enter your bot username my-bot
? Select your type of application Bot Application
? Select your programing language Java
? Select your framework Java (no framework)
? Select your build system Maven
? Enter your project groupId com.mycompany
? Enter your project artifactId bot-application
? Enter your base package com.mycompany.bot

Generating RSA keys...
   create rsa/publickey.pem
   create rsa/privatekey.pem
   create src/main/resources/config.yaml
   create src/main/resources/templates/gif.ftl
   create src/main/resources/templates/welcome.ftl
   create src/main/java/com/mycompany/bot/BotApplication.java
   create src/main/java/com/mycompany/bot/GifFormActivity.java
   create mvnw
   create mvnw.cmd
   create .mvn/wrapper/MavenWrapperDownloader.java
   create .mvn/wrapper/maven-wrapper.properties
   create pom.xml
   create .gitignore

[INFO] Scanning for projects...
[INFO]
[INFO] -------------------< com.mycompany:bot-application >--------------------
[INFO] Building bot-application 0.0.1-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ bot-application ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 3 resources
[INFO]
[INFO] --- maven-compiler-plugin:3.8.1:compile (default-compile) @ bot-application ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 2 source files to /home/ys/code/hello/target/classes
[INFO]
[INFO] --- maven-resources-plugin:2.6:testResources (default-testResources) @ bot-application ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory /home/ys/code/hello/src/test/resources
[INFO]
[INFO] --- maven-compiler-plugin:3.8.1:testCompile (default-testCompile) @ bot-application ---
[INFO] No sources to compile
[INFO]
[INFO] --- maven-surefire-plugin:2.12.4:test (default-test) @ bot-application ---
[INFO] No tests to run.
[INFO]
[INFO] --- maven-jar-plugin:2.4:jar (default-jar) @ bot-application ---
[INFO] Building jar: /home/ys/code/hello/target/bot-application-0.0.1-SNAPSHOT.jar
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  1.473 s
[INFO] Finished at: 2023-07-05T15:40:04+08:00
[INFO] ------------------------------------------------------------------------

You can now update the service account my-bot with the following public key:

-----BEGIN RSA PUBLIC KEY-----
MIICCgKCAgEAvFZi2h0yX7uScduCRD+tCKOJAaMB3bNUyjXv/5f+aCMfzhfx9JpS
...
XCXnaO2GT22pmjjqLLUKzw2hump2fpAka2l8+mc0UzZc658JcROClEMCAwEAAQ==
-----END RSA PUBLIC KEY-----

Please submit these details to your pod administrator.
If you are a pod administrator, visit https://mycompany.symphony.com/admin-console

Your Java project has been successfully generated !
 __   __     ___                 _
 \ \ / /__  / __|_  _ _ __  _ __| |_  ___ _ _ _  _
  \ V / _ \ \__ \ || | '  \| '_ \ ' \/ _ \ ' \ || |
   |_|\___/ |___/\_, |_|_|_| .__/_||_\___/_||_\_, |
                 |__/      |_|                |__/

Welcome to Symphony Generator v2.7.1
Application files will be generated in folder: /home/user/code/my-bot
______________________________________________________________________________________________________
? Enter your pod host mycompany.symphony.com
? Enter your bot username my-bot
? Select your type of application Bot Application
? Select your programing language Python

Generating RSA keys...
   create rsa/publickey.pem
   create rsa/privatekey.pem
   create requirements.txt
   create resources/logging.conf
   create .gitignore
   create readme.md
   create resources/config.yaml
   create resources/gif.jinja2
   create src/__main__.py
   create src/activities.py
   create src/gif_activities.py

No change to package.json was detected. No package manager install will be executed.

You can now update the service account my-bot with the following public key:

-----BEGIN RSA PUBLIC KEY-----
MIICCgKCAgEA059e0WKgmDVyazFxszs0Xty17iLemhyIrghwfn9XGEwvK40q/dE0
...
ST7yvzUeAy5Xkhad6ySvSviK9dt2Zl+FLVu3uwg1bMfMzXN2T44afp0CAwEAAQ==
-----END RSA PUBLIC KEY-----

Please submit these details to your pod administrator.
If you are a pod administrator, visit https://mycompany.symphony.com/admin-console

Your Python project has been successfully generated !
Install environment: python3 -m venv env
Activate virtual environment: source env/bin/activate
Install all required packages with: pip3 install -r requirements.txt
And run your bot with: python3 -m src

Configuration

The Symphony Messaging Generator creates a basic configuration file that works for fully cloud-hosted pods. If you have additional network requirements like a web proxy or an on-premise Key Manager and API Agent, refer to the complete configuration guide below for more details.

Create Service Account

For any bot to work, it requires a service account with a matching username and public key. The Symphony Messaging Generator creates a configuration file based on the answers supplied, including the bot username and the path to the generated key pair. These can be changed by modifying the config.yaml file. If you do not already have a service account set up, follow the instructions on this page to continue.

Test your Bot

Open the project in your preferred IDE and use the IDE's launch/debug feature to start the bot. Alternatively, continue using the command-line to launch the bot using the appropriate command.

# Maven + No Framework
# Edit package and main class if changed
./mvnw exec:java -Dexec.mainClass=com.mycompany.bot.BotApplication

# Maven + Spring
./mvnw spring-boot:run

# Gradle + No Framework
./gradlew run

# Gradle + Spring
./gradlew bootRun
# For the first run, create a virtual environment and install requirements
python3 -m venv env
pip3 install -r requirements.txt

# For subsequent runs, 
source env/bin/activate  # macOS or Linux
env\Scripts\activate.bat # Windows

# Run the bot
python3 -m src

The bot should start up successfully and the following log indicates that it is up and listening:

[main] INFO com.symphony.bdk.core.service.datafeed.impl.DatafeedLoopV2 - Start reading events from datafeed

symphony.bdk.core.service.datafeed.datafeed_loop_v2 - DEBUG - Starting datafeed V2 loop

Build your Bot

Now that you have a basic bot project ready, you can proceed to find out more about building bots using the respective kits:

Planning Your Bot

What is a Bot?

A bot on Symphony Messaging can be thought of as an automated version of a human performing specific tasks in Symphony Messaging chats. Most actions that an end user is able to perform in Symphony Messaging can be performed by a bot as well.

Each bot has a unique identity represented by a service account that has similar features to an end-user account such as a name and avatar.

Symphony Messaging Bots enable end users to benefit from innovative workflows and time-saving automations built on top of the Symphony Messaging platform.

How do Bots create workflows and automations?

The answer lies in Symphony Messaging's open REST API. Once authenticated, Symphony Messaging bots can leverage the APIs that enables bots to execute administrative functions such as creating chatrooms, managing users, and facilitating cross-pod connections. In addition, our APIs allow bots to perform messaging functions such as sending and receiving messages and signals.

For a full overview of the Symphony Messaging REST APIs continue here:

Next Steps

Before you begin your Bot development journey, it is important to consider the following when determining what type of Bot you build:

1. What are the goals of your Bot?

Before building your Bot, it's important that you identify the use cases that this Bot will serve. In other words, identify the ways in which this Bot will increase productivity, add meaningful color to your daily tasks, centralize information, reduce business pain points, and make working simpler for its users. To easily identify valuable use cases, ask yourself the following:

  • Are there any tasks in my daily workflow that are recurring and can be automated to assist me in my job?

  • Are there numerous sources of information that I check daily that can be centralized inside of Symphony Messaging?

  • Are there any tasks in my daily workflow that require manually sifting through large amounts of data?

  • Is there tedious data validation or compliance checks that I must perform when dealing with colleagues, clients, customers, or third-party vendors?

  • Do I have to manually collect and carefully collate unstructured data from colleagues, clients, customers, or third-party vendors?

2. Who is your Bot's target audience?

The type of Bot you build will depend on who is using and interacting with it. To identify your bot's audience, ask yourself the following:

  • Are the users of my bot internal or external counter-parties?

  • Will the bot be interacting with front-office or back-office employees mostly?

  • Is your bot interacting with a technical audience or business audience?

  • Will users interact with your bot via the Symphony Messaging Mobile App or on the desktop?

  • What languages does your audience speak?

  • Will your bot be performing bot-to-bot communication?

The more you understand your audience, the more you can understand their business pain points and in turn develop a better user-experience and bot-based solution.

3. What sort of interactions will your Bot have?

Users can interact with Bots in IMs, group chats, and in chatrooms. Before building your Bot, it's important to identify the types of interactions between users and your Bot:

Will your Bot be sending and receiving messages to and from users?

If so, you are looking to build a chatbot, which is a type of Bot that allows direct user interaction in the form of request/reply. You can learn more about rich chat-based workflows and building chatbots here:

If so, you are looking to build an interactive bot. Interactive bots leverage Elements to collect user data and feedback through forms, textfields, buttons, etc. You can learn more about interactive workflows and Elements here:

Will your Bot act as a notification system, without any chat or request/reply mechanisms?

If so, you are looking to build a headless bot. Headless bots can leverage webhooks or build custom notification handlers and formatters from external systems. You can learn more about headless bot workflows and notification handlers here:

4. What is your Bot's entry point?

Lastly, it's important to clearly define the lifecycle and scope of your bot's workflow:

  • What chats will my bot exist in?

  • Will these rooms be public, private, broadcast, or external (cross-pod)?

  • How can I define the scope of my bot in order to reduce unnecessary noise not relevant to my bot's workflow?

  • How can I clearly define the protocol for initiating my bot's workflow?

Continue here for a full list of Bot's best practices to establish Bot parameters, scope, and protocol:

Authentication

In order for bots to access the Symphony Messaging REST API and other Symphony Messaging resources, bots must first authenticate.

Token Management

The token you receive is valid for the lifetime of a session that is defined by your Pod's administration team. This ranges from 1 hour to 2 weeks.

You should keep using the same token until you receive an HTTP 401, at which you should re-authenticate and get a new token for a new session.

Note: Datafeeds survive session expiration, you do not need to re-create your datafeed if your session expires.

In order to obtain a valid Session Token and Key Manager Token, bots must call the Session Authenticate endpoint on the Pod and Key Manager Authentication endpoint on the Key Manager.

RSA Authentication Workflow

We recommend that bots follow the RSA authentication workflow in order to obtain valid Session and Key Manager Tokens:

Certificate-Based Authentication Workflow

For users that do not want to use RSA Authentication, bots can perform certificate-based authentication in order to obtain valid Session and Key Manager Tokens:

Truststores

You should only need to follow the below steps if your organisation uses self-signed custom certificates which are signed by an Internal Root Certificate Authority (Root CA).

Overview

In an enterprise environment, it is very common to have an internal root certificate authority (root CA) that signs all certificates used internally for TLS termination on HTTPS sites. While this trust is federated on the operating system level for all domain-connected Windows workstations, non-Windows servers are typically not configured the same way. Even on a Windows workstation, your Java runtime might not necessarily trust the enterprise root CA certificates.

If a Java application attempts to make a connection to an untrusted remote server, it will throw an SSLException and fail to connect.

Java Truststores

A Java truststore is a bundle of certificates used when establishing connections to remote servers (this could be internal or on the Internet). Each Java runtime ships with a default truststore called cacerts. Adding certificates directly into cacerts is usually not recommended unless there is a centrally-managed deployment process for all workstations and servers.

Bot Truststore

A bot requires connections to several servers like the key manager or API agent, as defined in your bot configuration. This configuration should also define the path to a custom truststore, which ensures that the trust relationship is maintained even as the bot's deployment location is moved, as long as the truststore file is moved together with the deployment artifacts.

BDK for Java

The BDK for Java uses the JKS format and the configuration is under the ssl section within the config.yaml:

If you are using the BDK for Java's Spring Starters, modify the appropriate application.yaml file to include the ssl section under the bdk section:

BDK for Python

The BDK for Python uses a concatenated PKCS format. Your config.yaml should look like this:

Certificate Chains

A typical internal certificate has has least 2 certificates in its chain - the certificate itself as well as the root CA certificate that signed it. Some enterprises will have intermediate CAs to form a 3-certificate chain. When Java establishes a connection to a remote server, it requires all signing certificates in that chain to be trusted. Hence, when building a truststore, you will need to import every signing certificate in the chain of each remote server that is not already present in the cacerts bundle.

Publicly-trusted signing certificates from established certificate authorities are already in cacerts so you will most likely not need to import certificates for services on the public Internet. However, if the Java runtime you are using pre-dates the existence of those certificates, then you will need to add them manually.

Building a Java Truststore

You can follow the same steps as the Java process below for WDK. For Python you can also follow these steps except instead of using keytool to import the certificates, simply concatenate all the certificates into a single file.

The keytool command that ships with all Java runtimes is used to manage truststores. You can build a new truststore or append to an existing one using the same command as follows:

Automatic: Convenience Shell Script

You may use the following convenience shell script to automatically build out a new truststore based on a list of servers. Please remember to include all possible connections that your bot requires. This may include: all Key Manager servers, all API Agent servers, the Symphony Messaging pod, any other third-party or internal endpoints that the bot will interact with.

Manual: Collating Certificates in Windows

If you have no access to a Linux-based shell, you can also manually perform this process within a Windows environment. All you need is a browser and a Java runtime with keytool installed.

Step 1: Visit an endpoint on each server in your browser and view the certificate

Step 2: Select the next signing certificate

In the Certification Path tab, select the end certificate's parent and View Certificate

Step 3: Export the certificate

In the Details tab, click Copy to File.. and select the Base-64 encoded X.509 option then save the certificate to disk.

Step 4: Repeat for the rest of the chain

Repeat Steps 2 and 3 until you have exported all signing certificates in the chain.

Step 5: Build the trust store

Launch command prompt and repeat this command for each exported certificate

You can verify the entries by using the -list option:

Symphony Messaging Key Manager APIs

Key Manager Authenticate API

In order to access Agent API endpoints, bots must be authenticated with the Pod and the Key Manager. To authenticate with the Key Manager, a bot must call the .

The offers a fast way to bootstrap your Symphony Messaging BDK project in Java and Python.

Prerequisite: Install NodeJS first, either or via

If you need additional inspiration, checkout our Symphony Messaging App Directory for examples of what has been built today:

Will your Bot need to collect structured/unstructured data through forms?

As we learned in , bots must be authenticated on the Pod in order to access Pod API endpoints. To make authenticated Pod API calls, bots must pass a valid Session Token as a header of each Pod API request.

We also learned in , that bots must be authenticated on the Key Manager in order to access Agent API endpoints. To make authenticated Agent API calls, Bots must pass a valid Session Token and Key Manager Token as headers of each Agent API request.

Key Manager Authenticate endpoint
Authentication
Symphony Messaging Generator
directly
nvm
Configuration
Creating a Service Account

Bot Developer Kit for Java

Bot Developer Kit for Python

Overview of REST API
https://symphony.com/resource/app-directory/
Conversational Bot
Interactive Elements
Interactive Bot
Headless Bot
Bots Best Practices
ssl:
  trustStore:
    path: path/to/truststore
    password: changeit
bdk:
  ssl:
    trustStore:
      path: path/to/truststore
      password: changeit
ssl:
  trustStore:
    path: path/to/truststore.pem
keytool -import -file my_cert_file -alias my_cert_alias -keystore my_truststore \
   -storepass my_store_password -trustcacerts
#!/bin/bash
SERVERS=my-key-manager-2:8443,my-agent:443,my-company.symphony.com:443
TRUSTSTORE_FILENAME=truststore
TRUSTSTORE_PASSWORD=changeit

command -v keytool >/dev/null 2>&1||{ echo >&2 "Please add keytool to \$PATH";exit 1;}

# Download all certificates from all servers
for SERVER in $(echo $SERVERS | sed "s/,/ /g")
do
  HOSTNAME="${SERVER%%:*}"
  openssl s_client -showcerts -verify 5 -connect $SERVER< /dev/null | \
    awk -v h=$HOSTNAME '/BEGIN/,/END/{ if(/BEGIN/){a++}; out=h"-"a-1".crt"; print >out}'
done

# Remove end certificates
rm *-0.crt

# Loop unique list of signing certificates
for cert in `shasum *-*.crt | sort -k1 | uniq -w41 | sort -k2 | awk '{print $2}'`
do
  keytool -import -file $cert -alias $cert -trustcacerts -noprompt \
    -keystore $TRUSTSTORE_FILENAME -storepass $TRUSTSTORE_PASSWORD
done

# Cleanup and verify truststore
rm *-*.crt
keytool -list -keystore $TRUSTSTORE_FILENAME -storepass $TRUSTSTORE_PASSWORD
keytool -import -file my_cert_file -alias my_cert_alias -keystore my_truststore \
  -storepass my_store_password -trustcacerts -noprompt
keytool -list -keystore my_truststore -storepass my_store_password
Overview of Pod API
Overview of Agent API
RSA Authentication Workflow
Certificate Authentication Workflow
here
here

Datafeed

Overview of Symphony Messaging Datafeed

Overview of Datafeed

The Symphony Messaging datafeed provides a stream of real-time messages and events for all conversations that a bot is a member of. Any event that occurs within a bot's scope will be captured and delivered to the bot by the datafeed. The datafeed forms the basis of all interactive and conversational bot workflows as it allows bots to directly respond to Symphony Messaging messages and events.

Datafeed Architecture

The following illustrates the relationship between your bot, datafeed, and Symphony Messaging's components:

  1. Bot creates datafeed via Symphony Messaging’s REST API

  2. Agent creates secure upstream connection with the Symphony Messaging Pod

  3. End user sends a message to a bot in a chatroom

  4. Pod delivers ‘MESSAGESENT’ event to Agent

  5. Bot reads datafeed via REST API

  6. Agent delivers ‘MESSAGESENT’ event payload to the Bot

Real-Time Events

Events are delivered to your bot via the datafeed as JSON objects. Each type of Symphony Messaging event corresponds to a different JSON payload.

For example, if a user sends your bot a message, an event of type 'MESSAGESENT' will be delivered to your bot through the datafeed:

{
    "id": "9rc1dr",
    "messageId": "Fd4Pc8xO5Vg6hVfzabFe2X___oyM1eXobQ",
    "timestamp": 1595365005847,
    "type": "MESSAGESENT",
    "initiator": {
        "user": {
            "userId": 344147139494862,
            "firstName": "Reed",
            "lastName": "Feldman",
            "displayName": "Reed Feldman (SUP)",
            "email": "reed.feldman@symphony.com",
            "username": "reedUAT"
        }
    },
    "payload": {
        "messageSent": {
            "message": {
                "messageId": "Fd4Pc8xO5Vg6hVfzabFe2X___oyM1eXobQ",
                "timestamp": 1595365005847,
                "message": "<div data-format=\"PresentationML\" data-version=\"2.0\" class=\"wysiwyg\"><p>hi</p></div>",
                "data": "{}",
                "user": {
                    "userId": 344147139494862,
                    "firstName": "Reed",
                    "lastName": "Feldman",
                    "displayName": "Reed Feldman (SUP)",
                    "email": "reed.feldman@symphony.com",
                    "username": "reedUAT"
                },
                "stream": {
                    "streamId": "IEj12WoWsfTkiqOBkATdUn___pFXhN9OdA",
                    "streamType": "IM"
                },
                "externalRecipients": false,
                "userAgent": "DESKTOP-43.0.0-10902-MacOSX-10.14.6-Chrome-83.0.4103.61",
                "originalFormat": "com.symphony.messageml.v2",
                "sid": "98202eac-dcf4-4b1e-a120-596db38319dc"
            }
        }
    }
}

Notice how each event returned by the datafeed has important metadata and attributes such as messageId, timestamp, (event) type, initiator, as well as the contents of the message itself inside of the payload object. Additionally, you can find the streamID corresponding to the message and also information regarding externalRecipients.

For a full list of the JSON payloads corresponding to each event type, continue here:

Handling Events using BDK

The BDK (Bot Developer Kit) comes bootstrapped with a DatafeedEventService class that handles all of the logic for creating/reading datafeeds via the API, has best practices for maintaining datafeeds, and also provides event handling architecture that makes it easy to orchestrate complex workflows and introduce custom business logic to your bot.

As a bot developer, all you have to do is to implement generic EventHandler classes, passing in a given event type as the type parameter for that class.

The following diagram shows the event handling workflow:

  1. Bot creates datafeed via Symphony Messaging’s REST API

  2. Agent creates secure upstream connection with the Symphony Messaging Pod

  3. End user sends a message to a bot in a chatroom

  4. Pod delivers ‘MESSAGESENT’ event to Agent

  5. Bot reads datafeed via REST API

  6. Agent delivers ‘MESSAGESENT’ event payload to the Bot

  7. Bot routes event to appropriate event listener/handler

Inside of onMessageSent() is where you implement your own business logic such as accessing a database, connecting to an external API, or reply back to your user by leveraging the Symphony Messaging API/BDK methods:

public class Example {

    public static void main(String[] args) { 
        // create bdk entry point
        final SymphonyBdk bdk = new SymphonyBdk(loadFromClasspath("/config.yaml"));
        
        // create listener to be subscribed
        final RealTimeEventListener listener = new RealTimeEventListener() {
            @Override
            public void onMessageSent(V4Initiator initiator, V4MessageSent event) {
                log.info("Message sent");
            }
        };

        // subscribe a listener
        bdk.datafeed().subscribe(listener);
       
        // start reading the datafeed 
        bdk.datafeed().start(); 
    }
}
from symphony.bdk.core.config.loader import BdkConfigLoader
from symphony.bdk.core.symphony_bdk import SymphonyBdk
from symphony.bdk.core.service.datafeed.real_time_event_listener import RealTimeEventListener

class RealTimeEventListenerImpl(RealTimeEventListener):

    async def on_message_sent(self, initiator, event):
        # message received, interact with it
        pass

async def run():
    async with SymphonyBdk(BdkConfigLoader.load_from_symphony_dir("config.yaml")) as bdk:
        datafeed_loop = bdk.datafeed()
        # subscribe your listener
        datafeed_loop.subscribe(RealTimeEventListenerImpl())
        # start reading the datafeed
        await datafeed_loop.start()

Conversational Workflow

As you can see, the datafeed acts as the backbone of your Bot. In many cases your Bot will be waiting for events to come in through the datafeed, which it constantly 'reads'. When an event or message comes through the datafeed, your bot will 'listen' for the event, extract the relevant data from the JSON payload and kick off its intended workflow.

While you can write all of this datafeed logic yourself, our dedicated BDK toolkits provide out-of-the-box datafeed support and event handling logic making it easy to bootstrap your bot and add custom business logic.

Deprecation notice of Legacy Datafeed API

The legacy agent/v4/datafeed API is out of support since April 30, 2023.

To facilitate this transition, a new feature called the datafeed bridge has been introduced in the Agent service so consumers of the deprecated APIs keep a functioning service.

  • This bridge is available starting with Agent 22.6 (June 2022) and can be enabled through the following configuration flag agent.df1ToDf2Bridge.enabled.

  • Since Agent release 23.6 (June 2023), this bridge is enabled by default, but could still be disabled through configuration.

  • Then, starting with Agent release 23.9 (September 2023), the bridge is always enabled.

Changes required to upgrade to v5 endpoints

The v5 endpoints are different from the v4 ones, so migrating requires changes in your code. If you are using our Java BDK, migrating to v5 is a simple configuration change.

Otherwise, the mapping between the API endpoints is the following:

  • Path /agent/v4/datafeed (deprecated)

    • Via a POST on the endpoint /agent/v4/datafeed/create, the datafeed is created and then the ID is persisted in a file, which is by default datafeed.id on the bot side

    • The bot subscribes to this ID to retrieve datafeed events; if it cannot be retrieved by using this ID, a new datafeed is created

    • Via a GET on the endpoint /agent/v4/datafeed/{id}/read, the list of events within the specific datafeed identified with {id} is returned

    • Deleting a datafeed is not supported

  • Path /agent/v5/datafeeds

    • Via a GET on the endpoint /agent/v5/datafeeds, is returned the list of already created IDs for a service account

    • Via a POST on the endpoint /agent/v5/datafeeds, the datafeed is created and the ID is not persisted on the bot side → Even if the bot is stale, a GET on the same endpoint will retrieve the ID to which the service account is subscribed

    • Via a POST on the endpoint /agent/v5/datafeeds/{id}/read with a parameter ackId (empty string at the first query), the endpoint returns: the list of events, a new ackId string → This ackId permits acknowledgement of the last query and retrieve all events since the last one. All events received between the last payload and the new request are queued and therefore retrieved.

    • Via a DELETE on /agent/v5/datafeeds/{id}, the datafeed specified with the {id} is deleted.

Certificate Authentication Workflow

This pages describes the implementation of certificate-based Authentication. For the API reference of Certificate Session Authenticate and Certificate Key Manager Authenticate, see the following API endpoints:

Note: The following authentication sequence is provided out of the box by our dedicated BDK and WDK toolkits. To learn more about authenticating using the SDKs or BDK proceed to one of following configuration guides:

Overview of Certificate-Based Authentication

Symphony Messaging allows you to authenticate on the Pod and Key Manager with a client certificate that is signed by a trusted root certificate. When a bot calls the Session Authenticate endpoint, the Pod examines the client certificate provided in the TLS session to identify the bot user and return a Session Token. The same process occurs when a bot authenticates on the Key Manager.

All Symphony Messaging network communications take place over TLS-protected HTTP. The network uses authentication methods that require a client-authenticated TLS connection.

Client certificate authentication in TLS is configured at the port level. Two distinct ports are required for client-authenticated and non-client-authenticated connections. The web and mobile endpoints listen on port 443 (the default port number for HTTPS connections). The API endpoints require a separate port, typically port 8444.

Summary

  1. The Admin upload a Signing certificate or Root certificate using the Admin portal.

  2. The Admin provides to the developer a child client certificate derived from the Signing or Root certificate

  3. The developer authenticates the Bot using the client certificate.

Note: It is also possible to directly upload a Client certificate in the Admin portal instead of a Signing or Root certificate.

1. Upload a Signing Certificate or Root Certificate

Please note the below steps can only be performed by a Symphony Messaging Pod Administrator as they will have the necessary administrator privileges to access the Administration Portal.

The certificate should be concerted to a CER or PEM format before it is uploaded

Once you have obtained a copy of your Root Certificate Authorities (CA) Public "Signing Certificate", you can upload it using the following steps:

  1. Once logged in click the Manage Certificates button then select Import

  2. Drag and drop your Certificate file into the popup window:

  1. Once you have uploaded the certificate file, click Import. If successful you will receive a confirmation message saying that the certificate has been uploaded successfully.

2. Generate a Client Certificate

You can use the following commands to generate the service account certificate. The certificate must use 4096 bits length.

$ openssl genrsa -aes256 -passout pass:$PASSWORD -out admin-key.pem 4096
$ openssl req -new -key admin-key.pem -passin pass:$PASSWORD -subj "/CN=$USERNAME/O=Symphony Communications LLC/OU=NOT FOR PRODUCTION USE/C=US" -out admin-req.pem
$ openssl x509 -req -sha256 -days 2922 -in admin-req.pem -CA $CA_CERT -CAkey $CA_KEY -passin pass:$CA_PASSWORD -out admin-cert.pem -set_serial 0x1
$ openssl pkcs12 -export -out admin.p12 -aes256 -in admin-cert.pem -inkey admin-key.pem -passin pass:$PASSWORD -passout pass:$OUTPUT_PASSWORD
  • USERNAME = Service account username

  • PASSWORD = Service account key password

  • CA_CERT = CA certificate file

  • CA_KEY = CA key file

  • CA_PASSWORD = CA key password

  • OUTPUT_PASSWORD = PKCS12 file password

Creating a Certificate Signing Request (CSR):

The following table shows the information you will need to provide to your PKI team:

The Common Name (CN) value must match the name of the Symphony Service Account you created, this should also use the same case value.

Details

Example Values

Certificate Type

Single Domain Certificate

Common Name (CN)

demo-bot1

Organization

Excelsior Bank

Department

Collaboration Services

Email

admin@bots.symphony.com

Locality

London

State / Province

London

Country

GB

Key Size

2048 bits

Creation of the service user in the Admin Portal

The Symphony Messaging Admin then creates a service user with a username that matches the Common Name of the certificate, as you can see in the example below:

3. Authenticate

$ curl --cert bot.user1.p12:mypassword
https://${symphony.url}/sessionauth/v1/authenticate
-X POST

A successful response:

{
  "name":"sessionToken",  
  "token":"SESSION_TOKEN"
}
$ curl --cert bot.user1.p12:mypassword
https://${symphony.url}/keyauth/v1/authenticate
-X POST

A successful response:

{
  "name":"keyManagerToken",
  "token":"KEY_MANAGER_TOKEN"
}

Pass the Session Token and Key Manager Token as headers for all subsequent API requests.

REST API Architecture

Overview of Symphony Messaging REST API Architecture

Symphony Messaging REST API is spread out over three main components: the Pod, API Agent and Key Manager. Let's take a closer look at these components below.

The Symphony Messaging Pod is a dedicated Symphony Messaging instance for each customer environment. It is a cloud-hosted component that handles all core operations necessary to provide the Symphony Messaging service to you. Since Symphony Messaging provides end-to-end encrypted messaging, all messages passed from user to user are fully encrypted at the time of sending, such that no Pod ever has access to the unencrypted contents of any message.

In addition, the Symphony Messaging Pod provides REST API endpoints in order for your bot to perform administrative functions on the Pod. You can read more about the Pod API here:

API Agent

The API Agent is the component responsible for encrypting and decrypting content sent from and to a bot. The Agent provides REST API endpoints that allow a bot to send and receive encrypted messages, acting as the intermediary between a bot and the Symphony Messaging Pod. In order to safely encrypt and decrypt these messages, the Agent server interacts the Key Manager which provides the keys used for encrypting and decrypting messages.

Read more about the Agent API here:

Key Manager

The Key Manager generates and stores encryption keys which are used to encrypt and decrypt messages by the Agent Server. The Key Manager provides an authentication API that provides a unique Key Manager Token to a calling bot. This token is used to encrypt/decrypt messages on the Agent Server.

Read more about the Key Manager API here:

Interacting with the Components

The three components above all interact with each other in order to create Symphony Messaging's secure messaging service. Let's take a closer look at the sequence of API calls a bot must make in order to send and receive encrypted messages on Symphony Messaging.

The sequence of API calls and component interaction is illustrated below:

  • 1a. If successful, the bot will receive a valid Session Token. This Session Token must be passed along with all subsequent Symphony Messaging API requests destined for the Agent or the Pod.

  • 2a. If successful, the bot will receive a valid Key Manager Token. This Key Manager Token must be passed along with all subsequent Symphony Messaging API requests destined just for the Agent.

  • 4. At this point, the Agent Server calls the Key Manager and requests the bot's encryption keys.

  • 5. Next, the Agent Server validates the bot's Key Manager Token.

  • 6. If successful, the Agent will encrypt the payload sent by the bot and will forward the encrypted message up to the Pod where it will be routed to the intended user or chatroom. The message will remain encrypted until it reaches its final destination.

For an even more detailed explanation, enroll in our Developer Certification Program:

On-Premise Deployment

For our enterprise customers, the API Agent and Key Manager components are deployed on-premise, while the Pod is always deployed in the cloud. Your bot or REST API caller is an application that must be deployed on-premise in this scenario.

An visual representation showing an on-premise deployment of Symphony Messaging components is shown below:

In-Cloud Deployment

For our smaller customers, the API Agent and Key Manager may be co-hosted with the Pod in the cloud. Your bot or REST API caller can either be deployed on-premise or in your own cloud environment.

A visual representation showing an in-cloud deployment of Symphony Messaging components is shown below:

Bot Permissions

List of Bot Permissions

This page lists the available roles and the associated privileges that may be required for certain endpoints:

Individual Role

User Provisioning Role

Scope Management Role

Content Management Role

Expression Filter Policy Management Role

Audit Trail Management Role

Group Manager Role (Distribution List)

Malware Scan State User Role

Malware Scan Manager Role

Overview of Pod API

The Symphony Messaging Pod API is used to build tools in order to manage and administer Symphony Messaging for your organization. The following guide includes API collections that exist on the Pod:

If successful, the Pod API returns a Session Token which is valid for up to two weeks. This Session Token must be passed along with every subsequent Pod API request. You can read more about Authentication and Token management here:

The User APIs query and manage users on the Pod. These APIs can be used to do the following:

  • Search users

  • List all users

  • Create users

  • Update user information

  • List user features

  • Add/Remove user roles

  • List user roles

  • List user audit trail

The Stream APIs create and manage IMs and chat rooms. These APIs can be used to do the following:

  • Create IM

  • Create chatrooms

  • Search for rooms

  • Get room info

  • Deactivate room

  • List room members

  • Add/Remove room members

  • Promote/Demote user to room owner

The Connection APIs manage user connections. These APIs can be used to do the following:

  • Get connection

  • List connections

  • Create connections

  • Accept connection

  • Reject connection

  • Remove connection

The Presence APIs manage presence status for users. These APIs can be used to do the following:

  • Get/Set user presence

  • Get all users presence

  • Register presence interest

  • Create presence feed

  • Read presence feed

  • Delete presence feed

Overview of Streams

A Stream is like a container for messages exchanged between two or more users via a given 1 to 1 chat (IM) or chat room.

On the Symphony Messaging web or desktop client, this ID can be found by clicking on the timestamp of any message in the conversation. This will open the Message Status module, where the Conversation ID can be found, as shown in the following picture.

The Conversation ID in the Symphony Messaging Web Client is in Standard Base64 encoding and need to be converted to be URLSafe. Conversation IDs returned in API responses are already URLSafe Base64 encoding.

URLSafe Base64 Conversion

To obtain the URLSafe Base64 Conversation ID:

  • Replace forward slashes / with underscores _

  • Replace pluses + with minuses -

  • Ignore any trailing equal signs =

For example, the URLSafe Base64 encoding oflX1hwfmQ+AK/k/a/BB0y2n///q2+0KfbdA== converts to lX1hwfmQ-AK_k_a_BB0y2n___q2-0KfbdA.

The following list shows the existing endpoints:

Deprecation Notice v4/datafeed (known as Datafeed v1) The legacy Datafeed v1 service will no longer be supported on April 30, 2023. Please read for more information on this transition. Please reach out to your Technical Account Manager or to the Developer Relations team for more information.

Symphony Messaging provides a Datafeed API that allows bots to easily and datafeeds.

Once a bot has created a datafeed, it has access to all of the within its scope, acting as a secure channel between a bot and all activity happening in the Symphony Messaging Pod. Additionally, all messages and events within a bot's scope are encrypted by the Agent before reaching your bot. That way the bot is the only one who can access the contents of these events and messages being delivered.

After the DatafeedEventService creates/reads from the datafeed API, it categorizes each event based on its event type seen , and dispatches the event downstream to a generic event handler class. For example, If a user sends a message to bot inside a chatroom, the event will be read by the datafeed, and dispatched downstream to the EventHandler class that that takes MessageEvent in as a type parameter. Further the handle() method belonging to your EventHandler class will be called each type that event type is read by the datafeed.

This has an impact on you if some of your automations or bots are still using this API. Please upgrade to the new APIs.

We encourage you to migrate your bots to use the new . The bridge is a temporary solution, which objective is to facilitate the migration. If you use the BDK in or , the migration between v4 and v5 is automatic. We advise you to take this opportunity to migrate your bots to the BDK if you haven’t done so.

Session Auth:

Key Manager Auth:

Navigate to the Symphony Admin Console for your Pod (e.g. ), then log in with your credentials

To authenticate on the Pod the bot must call the Session Auth endpoint: . Pass along the client certificate provided in the TLS session, returning a Session Token:

To authenticate on the Key Manager, the bot must call the Key Manager Auth endpoint: . Pass along the client certificate provided in the TLS session, returning a Key Manager Token:

Pod

1. First, a bot must authenticate with the Pod. It does so by calling the endpoint.

2. Next, a bot must authenticate with the Key Manager. It does so by calling the .

3. If the bot wants to send a message, the bot will call the on the Agent API and pass both Session Token and Key Manager Token as a part of the request.

Navigate to to learn more about how bots process messages and other real time events

Symphony Messaging Pod APIs

Session Authenticate API

In order to access Pod API endpoints, bots must be authenticated with the Pod. To do so, a Bot must call the .

User APIs

The full list of Users API endpoints and their corresponding reference guide can be found at and .

Stream APIs

The full list of Streams API endpoints and their corresponding reference guide can be found here:

Connection APIs

The full list of Connections API endpoints and their corresponding reference guide can be found here:

Presence APIs

The full list of Presence API endpoints and their corresponding reference guide can be found here:

Each Stream has a unique ID also known as a Conversation ID, that is returned when the IM or chat room is created using the , , and endpoints respectively. This ID can then be used in subsequent endpoints to perform operations on the IM or chat room.

create
read
events
Real-Time Events
/agent/v5/datafeeds
/agent/v5/datafeeds
Java
Python
https://developers.symphony.com/restapi/main/bot-authentication/session-authenticate
https://developers.symphony.com/restapi/main/bot-authentication/key-manager-authenticate
Configure your Bot for BDK 2.0 for Java
https://mypod.symphony.com/?admin
https://developers.symphony.com/restapi/main/bot-authentication/session-authenticate
https://developers.symphony.com/restapi/main/bot-authentication/key-manager-authenticate
Overview of Pod API
Overview of Agent API
Overview of Key Manager API
Session Authenticate
Key Manager Authenticate endpoint
Create Message endpoint
Developer Certification
Overview of Datafeed
below
above

Privileges

LOGIN_WITH_PASSWORD

Privileges

ACCESS_ADMIN_API

ACCESS_CERT_PROVISIONING_API

ACCESS_USER_PROVISIONING_API

ACTIVATE_AND_DEACTIVATE_ANY_ROOM

ACTIVATE_SERVICE_ACCOUNT

ADD_AND_REMOVE_USERS_IN_ANY_ROOM

ADMIN_PRESENCE_UPDATE

ADMIN_RESET_USER_PASSWORD

BULK_MANAGE_USERS

CREATE_APPS

CREATE_END_USER_ACCOUNT

CREATE_SERVICE_ACCOUNT

DEACTIVATE_END_USER_ACCOUNT

DEACTIVATE_SERVICE_ACCOUNT

LOGIN_WITH_SHARED_SECRET

MODIFY_APPS MODIFY_APPS_ENTITLEMENTS

MODIFY_END_USER_ACCOUNT

MANAGE_BLACKLIST

MANAGE_USER_DELEGATES

MANAGE_INFO_BARRIERS

MODIFY_END_USER_ENTITLEMENTS

MODIFY_USER_APPS_ENTITLEMENTS

MODIFY_USER_DISCLAIMERS

MODIFY_SERVICE_ACCOUNT

MODIFY_SERVICE_USER_ENTITLEMENTS

PROMOTE_AND_DEMOTE_OWNERS_IN_ANY_ROOM

VIEW_ANY_STREAM_DETAILS

VIEW_ANY_STREAM_MEMBERSHIP

VIEW_APPS VIEW_APPS_ENTITLEMENTS

VIEW_BLACKLIST VIEW_DISCLAIMER_AUDIT_TRAIL

VIEW_INFO_BARRIERS

VIEW_PAST_STREAM_MEMBERS

VIEW_POD_DISCLAIMERS

VIEW_PROFANITY_ENFORCEMENT_AUDIT_TRAIL

VIEW_USAGE_POLICY_DETAILS

VIEW_USER_APPS_ENTITLEMENTS

VIEW_USER_AUDIT_TRAIL VIEW_USER_DETAIL

Privileges

ACCESS_ADMIN_API

VIEW_USER_AUDIT_TRAIL

MANAGE_ROLE_SCOPES

VIEW_ROLE_SCOPES

VIEW_USER_DETAIL

Privileges

ACCESS_ADMIN_API

IMPORT_MESSAGES

LOGIN_WITH_SHARED_SECRET SUPPRESS_MESSAGE

VIEW_MANAGE_MESSAGES

VIEW_USER_DETAIL VIEW_USER_ENTITLEMENTS

Privileges

ACCESS_ADMIN_API

DLP_CRYPTO_KEY

MANAGE_EXPRESSION_FILTERS

VIEW_DLP_VIOLATION

VIEW_PROFANITY_ENFORCEMENT_AUDIT_TRAIL

VIEW_EXPRESSION_FILTERS

VIEW_USER_DETAIL MANAGE_FILE_EXTENSIONS VIEW_USER_ENTITLEMENTS

Privileges

VIEW_PRIVILEGED_USER_AUDIT_TRAIL

ACCESS_ADMIN_API

Privileges

VIEW_USER_DETAIL

ACCESS_ADMIN_API CAN_CREATE_DISTRIBUTION_LIST

Privileges

SET_MALWARE_SCAN_STATE VIEW_MALWARE_SCAN_STATE

Privileges

VIEW_MALWARE_SCAN_CONFIG ACCESS_ADMIN_API VIEW_MALWARE_SCAN_AUDIT_TRAIL SET_MALWARE_SCAN_CONFIG VIEW_MALWARE_SCAN_STATE

MessageML Basics

Session Authenticate endpoint
Authentication
https://developers.symphony.com/restapi/main/users
https://developers.symphony.com/restapi/main/user-management
https://developers.symphony.com/restapi/main/streams-conversations
https://developers.symphony.com/restapi/main/connections
https://developers.symphony.com/restapi/main/presence
Create Instant Message
Create Non-inclusive Instance Message
Create Room v3
Create IM
Create IM non-inclusive
Create Room v3
Update Room v3
Room Info v3
De/Re-activate Room
Room Members
Add Member
Share
Remove Member
Promote Owner
Demote Owner
Search Rooms v3
List User Streams
Stream Info v2
List Streams for Enterprise v2
Stream Members

Overview of Agent API

The Symphony Messaging Agent is responsible for encryption and decryption of messages and content sent to and from a bot. As a result, the Agent API is used to build applications that send and receive messages and content. The following guide includes API collections that exist on the Agent:

The Message APIs create, read and search messages on the Pod. These APIs can be used to do the following:

  • Get messages

  • Create messages

  • Get attachments

  • List attachments

  • Import messages

  • Suppress messages

  • Search messages

  • Get message status

  • List message receipts

The Datafeed APIs create and manage real-time event streams from the Pod to your bot. These APIs can be used to do the following:

  • Create Datafeed

  • Read Datafeed

For more information on how Symphony Messaging Datafeeds allow your bot to create rich and interactive workflows, navigate here:

The Signal APIs create and manage tailored alerts based on mention or tag criteria. These APIs can be used to do the following:

  • List signals

  • Get signal details

  • Create a signal

  • Subscribe/Unsubscribe to a signal

  • List signal subscribers

This group of APIs perform testing and obtain diagnostics regarding the health of Symphony Messaging components. These APIs can be used to do the following:

  • Perform a component health check

  • Obtain Agent Info

  • Perform an echo test

  • Get session info

RSA Authentication Workflow

This pages describes the implementation of RSA Authentication. For the API reference of RSA Session Authenticate and Key Manager Authenticate, see the following API endpoints:

Note: The following authentication sequence is provided out of the box by our dedicated SDKs and BDK. To learn more about authenticating using the SDKs or BDK proceed to one of following configuration guides:

Summary

The Authentication process requires the following steps:

  1. The user creates a public/private RSA key pair.

  2. The admin imports the public key into the pod using the Admin Console or public APIs.

  3. The user creates a short-lived JWT (JSON Web Token) and signs it with their private key.

  4. The bot makes a call the the authentication endpoints. Here, the server checks the signature of the JWT against the public key and returns an authentication token.

Session Token Management

The token you receive is valid for the lifetime of a session that is defined by your pod's administration team. This ranges from 1 hour to 2 weeks.

You should keep using the same token until you receive a HTTP 401, at which you should re-authenticate and get a new token for a new session.

Supported Ciphers for the SSL/TLS session

Symphony only supports the following cipher suites:

ECDHE-RSA-AES256-GCM-SHA384 (Preferred)

ECDHE-RSA-AES128-GCM-SHA256

DHE-RSA-AES256-GCM-SHA384

DHE-RSA-AES128-GCM-SHA256

1. Create an RSA Key Pair

The public/private key pair for signing authentication requests requires the following:

  • A X.509 format for public keys and PKCS#1 or PKCS#8 for private keys

  • PEM-encoded keys

Note: This script requires the openssl package.

Generate the PKCS#1 keys manually using the following commands:

$ openssl genrsa -out mykey.pem 4096
$ openssl rsa -in mykey.pem -pubout -out pubkey.pem

Generate the PKCS#8 keys manually using the following commands. You can provide the Service Account's username as the Common Name (CN) but it is not a mandatory requirement.

$ openssl genrsa -out privatekey.pem 4096
$ openssl req -newkey rsa:4096 -x509 -key privatekey.pem -out publickey.cer
$ openssl pkcs8 -topk8 -nocrypt -in privatekey.pem -out privatekey.pkcs8
$ openssl x509 -pubkey -noout -in publickey.cer > publickey.pem

Sign the authentication request using either privatekey.pkcs8 or privatekey.pem, depending on the support available in the JWT library.

The file publickey.pem is the public key. This is the key you will import into the pod in step 2.

2. Import Public Key into the Pod

Please note the below steps can only be performed by a Symphony Messaging Pod Administrator as they will have the necessary administrator privileges to access the Administration Portal.

Navigate to the Admin Console and create a new Service Account. Copy the contents of the pubkey.pem file you just created and paste into the textbox under the Authentication section:

Add your bot's basic information:

If successful, you should see the following:

3. Generate a signed JWT Token

To authenticate on the Pod and the Key Manager, the bot must call the authentication endpoints, passing a short-lived JWT token in the body of the request. The JWT token must contain the following:

  • a subject matching the username of the user to authenticate

  • an expiration time of no more than 5 minutes from the current timestamp (needed to prevent replay attacks)

  • a signature by a private RSA key matching a public key stored for the user in the Pod

The following script generates the authentication request:

package com.symphony.util.jwt;

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.bouncycastle.asn1.pkcs.RSAPrivateKey;
import org.bouncycastle.crypto.params.RSAPrivateCrtKeyParameters;
import org.bouncycastle.crypto.util.PrivateKeyInfoFactory;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import org.bouncycastle.util.io.pem.PemObject;
import org.bouncycastle.util.io.pem.PemReader;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.StringReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.GeneralSecurityException;
import java.security.Key;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
import java.util.Date;
import java.util.stream.Stream;


/**
 * Class used to generate JWT tokens signed by a specified private RSA key.
 * Libraries needed as dependencies:
 *  - BouncyCastle (org.bouncycastle.bcpkix-jdk15on) version 1.59.
 *  - JJWT (io.jsonwebtoken.jjwt) version 0.9.1.
 * 
 * 
 */
public class JwtHelper {

  // PKCS#8 format
  private static final String PEM_PRIVATE_START = "-----BEGIN PRIVATE KEY-----";
  private static final String PEM_PRIVATE_END = "-----END PRIVATE KEY-----";

  // PKCS#1 format
  private static final String PEM_RSA_PRIVATE_START = "-----BEGIN RSA PRIVATE KEY-----";
  private static final String PEM_RSA_PRIVATE_END = "-----END RSA PRIVATE KEY-----";


  /**
   * Get file as string without spaces
   * @param filePath: filepath for the desired file.
   * @return
   */
  public static String getFileAsString(String filePath) throws IOException {
    StringBuilder message = new StringBuilder();
    String newline = System.getProperty("line.separator");

    if (!Files.exists(Paths.get(filePath))) {
      throw new FileNotFoundException("File " + filePath + " was not found.");
    }

    try (Stream<String> stream = Files.lines(Paths.get(filePath))) {

      stream.forEach(line -> message
          .append(line)
          .append(newline));

      // Remove last new line.
      message.deleteCharAt(message.length() -1);
    } catch (IOException e) {
      System.out.println(String.format("Could not load content from file: %s due to %s",filePath, e));
      System.exit(1);
    }

    return message.toString();
  }

  /**
   * Creates a JWT with the provided user name and expiration date, signed with the provided private key.
   * @param user the username to authenticate; will be verified by the pod
   * @param expiration of the authentication request in milliseconds; cannot be longer than the value defined on the pod
   * @param privateKey the private RSA key to be used to sign the authentication request; will be checked on the pod against
   * the public key stored for the user
   */
  private static String createSignedJwt(String user, long expiration, Key privateKey) {

    return Jwts.builder()
        .setSubject(user)
        .setExpiration(new Date(System.currentTimeMillis() + expiration))
        .signWith(SignatureAlgorithm.RS512, privateKey)
        .compact();
  }

  /**
   * Create a RSA Private Key from a PEM String. It supports PKCS#1 and PKCS#8 string formats
   */
  private static PrivateKey parseRSAPrivateKey(String privateKeyFilePath) throws GeneralSecurityException, IOException {
    String pemPrivateKey = getFileAsString(privateKeyFilePath);
    try {

      if (pemPrivateKey.contains(PEM_PRIVATE_START)) {              // PKCS#8 format

        String privateKeyString = pemPrivateKey
            .replace(PEM_PRIVATE_START, "")
            .replace(PEM_PRIVATE_END, "")
            .replace("\\n", "\n")
            .replaceAll("\\s", "");
        byte[] keyBytes = Base64.getDecoder().decode(privateKeyString.getBytes(StandardCharsets.UTF_8));
        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
        KeyFactory fact = KeyFactory.getInstance("RSA");
        return fact.generatePrivate(keySpec);

      } else if (pemPrivateKey.contains(PEM_RSA_PRIVATE_START)) {   // PKCS#1 format

        try (PemReader pemReader = new PemReader(new StringReader(pemPrivateKey))) {
          PemObject privateKeyObject = pemReader.readPemObject();
          RSAPrivateKey rsa = RSAPrivateKey.getInstance(privateKeyObject.getContent());
          RSAPrivateCrtKeyParameters privateKeyParameter = new RSAPrivateCrtKeyParameters(
              rsa.getModulus(),
              rsa.getPublicExponent(),
              rsa.getPrivateExponent(),
              rsa.getPrime1(),
              rsa.getPrime2(),
              rsa.getExponent1(),
              rsa.getExponent2(),
              rsa.getCoefficient()
          );

          return new JcaPEMKeyConverter().getPrivateKey(PrivateKeyInfoFactory.createPrivateKeyInfo(privateKeyParameter));
        } catch (IOException e) {
          throw new GeneralSecurityException("Invalid private key.");
        }

      } else {
        throw new GeneralSecurityException("Invalid private key.");
      }
    } catch (Exception e) {
      throw new GeneralSecurityException(e);
    }
  }

  public static String createJwt(String username, String privateKeyFilePath) throws IOException, GeneralSecurityException {
    final long expiration = 300000L;
    final PrivateKey privateKey = parseRSAPrivateKey(privateKeyFilePath);
    return createSignedJwt(username, expiration, privateKey);
  }

  public static void main(String[] args) throws IOException, GeneralSecurityException {
    final String username = System.getProperty("user");
    final String privateKeyFile = System.getProperty("key");

    final String jwt = createJwt(username, privateKeyFile);
    System.out.println(jwt);
  }
}
"""
0) Use python 3

1) Install python dependency required to run this script by:
    pip install python-jose

2) The .pem file used in this script was generated from the previous step of this tutorial.

3) Create a create_jwt.py file, copy and paste the content you see here. Place your private key .pem file in the same directory as this script.

4) Change the value of \'sub\' to your Symphony service account username. Change the filename of the .pem file to the filename of your .pem file.

5) Generate jwt token simply by:
    python create_jwt.py

6) You will see the jwt token in terminal output.
"""

from jose import jwt
import datetime as dt


def create_jwt():
    private_key = get_key()
    expiration_date = int(dt.datetime.now(dt.timezone.utc).timestamp() + (5 * 58))
    payload = {
        'sub': "username_of_service_account",
        'exp': expiration_date
    }
    encoded = jwt.encode(payload, private_key, algorithm='RS512')
    print(encoded)


def get_key():
    with open('filename_of_private_key.pem', 'r') as f:
        content =f.readlines()
        key = ''.join(content)
        return key


if __name__ == '__main__':
    create_jwt()
// Based on https://github.com/jwtk/njwt
'use strict';

let crypto = require('crypto');

function nowEpochSeconds() {
    return Math.floor(new Date().getTime() / 1000);
}

function base64urlEncode(str) {
    return new Buffer(str)
        .toString('base64')
        .replace(/\+/g, '-')
        .replace(/\//g, '_')
        .replace(/=/g, '');
}

function Jwt(username, signingKey) {

    this.header = {};
    this.body = {};
    this.header.alg = 'RS512';
    this.body.sub = username
    this.body.exp = (nowEpochSeconds() + (5 * 60)); // five minutes in seconds
    this.signingKey = signingKey;

    return this;
}

Jwt.prototype.sign = function sign(payload, cryptoInput) {
    let buffer = crypto.createSign('RSA-SHA512').update(payload).sign(cryptoInput);

    return base64urlEncode(buffer);
};

Jwt.prototype.compact = function compact() {

    let segments = [];
    segments.push(base64urlEncode(JSON.stringify(this.header)));
    segments.push(base64urlEncode(JSON.stringify(this.body)));
console.log("segments ", segments);
console.log("segments join ", segments.join('.'));
console.log("Signing key ", this.signingKey);
    this.signature = this.sign(segments.join('.'), this.signingKey);
    segments.push(this.signature);

    return segments.join('.');
};

const secret = "-----BEGIN RSA PRIVATE KEY-----\n" +
"...REDACTED..."
"-----END RSA PRIVATE KEY-----";

const user = 'bot.user1';
const jwt = new Jwt(user, secret);
const jws = jwt.compact();

console.log("========== JWS ==========")
console.log(jws);
using Microsoft.IdentityModel.Tokens;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.OpenSsl;
using System;
using System.Configuration;
using System.IdentityModel.Tokens.Jwt;
using System.IO;
using System.Security;
using System.Security.Claims;
using System.Security.Cryptography;

namespace Symphony.Util.Jwt
{
    /// <summary>
    /// Class used to generate JWT tokens signed by a specified private RSA key.
    /// Libraries needed as dependencies:
    ///     - BouncyCastle version >= 1.8.5
    ///     - System.IdentityModel.Tokens.Jwt version >= 5.4.0
    /// </summary>
    public class JwtHelper
    {
        /// <summary>
        /// Creates a JWT with the provided user name and expiration date, signed with the provided private key.
        /// </summary>
        /// <param name="user">The username to authenticate; will be verified by the pod.</param>
        /// <param name="expiration">Expiration of the authentication request in milliseconds; cannot be longer than the value defined on the pod.</param>
        /// <param name="privateKey">The private RSA key to be used to sign the authentication request; will be checked on the pod against the public key stored for the user.</param>
        public static string CreateSignedJwt(string user, double expiration, SecurityKey privateKey)
        {
            var handler = new JwtSecurityTokenHandler
            {
                SetDefaultTimesOnTokenCreation = false
            };

            var tokenDescriptor = new SecurityTokenDescriptor
            {
                Expires = DateTime.Now.AddMilliseconds(expiration),
                Subject = new ClaimsIdentity(new[] {
                    new Claim("sub", user)
                }),

                SigningCredentials = new SigningCredentials(privateKey, SecurityAlgorithms.RsaSha512, SecurityAlgorithms.Sha512Digest)
            };
            SecurityToken token = handler.CreateToken(tokenDescriptor);
            return handler.WriteToken(token);
        }

        /// <summary>
        /// Create a RSA Private Key from a PEM String. It supports PKCS#1 and PKCS#8 string formats.
        /// </summary>
        public static SecurityKey ParseRSAPrivateKey(String privateKeyFilePath)
        {
            if (!File.Exists(privateKeyFilePath))
            {
                throw new FileNotFoundException($"File {privateKeyFilePath} was not found");
            }

            var cryptoServiceProvider = new RSACryptoServiceProvider();
            using (var privateKeyTextReader = new StringReader(File.ReadAllText(privateKeyFilePath)))
            {
                object rsaKey = null;
                try
                {
                    rsaKey = new PemReader(privateKeyTextReader).ReadObject();
                }
                catch (Exception)
                {
                    throw new SecurityException("Invalid private key.");
                }


                RsaPrivateCrtKeyParameters privateKeyParams = null;
                // PKCS#8 format.
                if (rsaKey is RsaPrivateCrtKeyParameters)
                {
                    privateKeyParams = (RsaPrivateCrtKeyParameters)rsaKey;
                }
                // PKCS#1 format
                else if (rsaKey is AsymmetricCipherKeyPair)
                {
                    AsymmetricCipherKeyPair readKeyPair = rsaKey as AsymmetricCipherKeyPair;
                    privateKeyParams = ((RsaPrivateCrtKeyParameters)readKeyPair.Private);
                }
                else
                {
                    throw new SecurityException("Invalid private key.");
                }

                var parms = new RSAParameters
                {
                    Modulus = privateKeyParams.Modulus.ToByteArrayUnsigned(),
                    P = privateKeyParams.P.ToByteArrayUnsigned(),
                    Q = privateKeyParams.Q.ToByteArrayUnsigned(),
                    DP = privateKeyParams.DP.ToByteArrayUnsigned(),
                    DQ = privateKeyParams.DQ.ToByteArrayUnsigned(),
                    InverseQ = privateKeyParams.QInv.ToByteArrayUnsigned(),
                    D = privateKeyParams.Exponent.ToByteArrayUnsigned(),
                    Exponent = privateKeyParams.PublicExponent.ToByteArrayUnsigned()
                };

                cryptoServiceProvider.ImportParameters(parms);
            }

            return new RsaSecurityKey(cryptoServiceProvider.ExportParameters(true));
        }

        public static string CreateJwt(string username, string privateKeyFilePath)
        {
            double expiration = 300000; // 5 minutes = 5*60*1000
            SecurityKey privateKey = ParseRSAPrivateKey(privateKeyFilePath);
            return CreateSignedJwt(username, expiration, privateKey);
        }

        static void Main(string[] args)
        {
            string username = ConfigurationManager.AppSettings.Get("user");
            string privateKeyFile = ConfigurationManager.AppSettings.Get("key");

            string jwt = CreateJwt(username, privateKeyFile);
            Console.WriteLine(jwt);
            Console.WriteLine("Press enter to exit...");
            Console.ReadLine();
        }
    }
}

The output of the script is a JWT:

eyJhbGciOiJSUzUxMiJ9.eyJzdWIiOiJib3QudXNlcjEiLCJleHAiOjMwNDMwOTY0ODV9.X9vZReZigFtJ8NDsaJ9viUp2jtc-_ktVFLm17ubEzmSJbHXS_LNy5nL6E6R8GY71g8Vuonb8qSIwy8zoR_TcUvuPAQLxCAlvQn96jFnjg4aFO3kWkFMLFgwJWWR4hn2UocdTS_pu7ROafn6rjvLJdKGEWDOHKw6JX2_Qj3uzU3LeAFhUVU8Tmop3A2OTVUkPlWJwJimIas66kFgq61uGps8RT9YMs74bxGvOJvInidK2N_dqJMDPgb4ySOBHewlhe1ziUWM-21HDq1RvmadTWoPRKRXdt4oPRoxr4KRgmluaQpz8njL7Em9Sh1bCKJWuIjlXQPOcF3SFibbAcLwr40UnT2sM2LMJtkj0BHIU_5Ans0fN1x8hKtfWX_ArzLJTCBCCqswmq8Q3vxo0-SHe33Idy99TfkrY-C8G-fgPFvs9L7695MOcYAq8SpbZQlX-anpcqLQfsw6V-V0ZEAUeSHpnZrHvwmQjEmU9wXWzvAgCpF9kEt_I4Hpu8DTx2VzVj7CRU1Lu5NPHoESjI6VKJWcCH68TvkBB88jJqflXcQfbLUdK1sjDwDKl3BurmGBZSlD0ymuBXaQe4yol4zxXzSuWo6VCy5ykXee0mZm5t9-9wJujcjnGyKjNNSVLhajrmo6BRDN86I_xgV33SHgdrJKyQCO8LzUK4ArEMYlEY0I

4. Authenticate

Obtain a valid Session Token by making a POST request to your company's Session Auth endpoint:

$ curl -d '{"token":"eyJhbGciOiJSUzUxMiJ9...ik0iV6K9FrEhTAf71cFs"}' https://${symphony.url}:443/login/pubkey/authenticate

A successful response:

{"token":"eyJhbGciOiJSUzUxMiJ9...7oqG1Kd28l1FpQ","name":"sessionToken"}

Obtain a valid Key Manager Token by making a POST request to your company's Key Manager Auth endpoint:

$ curl -d '{"token":"eyJhbGciOiJSUzUxMiJ9...ik0iV6K9FrEhTAf71cFs"}' https://${symphony.url}:443/relay/pubkey/authenticate

A successful response:

{"token":"0100e4fe...REDACTED...f729d1866f","name":"keyManagerToken"}

Replace/Revoke Key

You can replace the public key pubkeyA for a user with a new key, pubkeyB (for example, as part of an organization's key rotation schedule). Note the following outcomes:

  • When a key is replaced, the key pubkeyA becomes the user's previous key, and the newly uploaded pubkeyB becomes the current key.

  • The previous key is valid for 72 hours, but you can extend that period indefinitely in intervals of 72 hours.

  • While the previous key is valid, both keys can be used for authentication. When it expires, it can no longer be used to authenticate the user.

  • A user can have at most one previous key.

Alternatively, you can revoke a user key (current or previous), for example, if the key is compromised. Note the following outcomes:

  • When a key is revoked, it can no longer be used for authentication.

  • If a user has a non-expired previous key and their current key is revoked, the previous key becomes the new current key.

  • When a key is revoked, the user's sessions initiated with RSA authentication are invalidated.

To replace/revoke a key, navigate to the Bot's account in the admin portal > RSA > Replace or Revoke:

You can also use the following REST API call to programmatically replace a public key:

curl -H 'sessionToken: eyJhbGciOiJSUzUxMiJ9...O3iq8OEkcnvvMFKg' -d '{

  {
    "currentKey": {
      "key": "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhki...WMCAwEAAQ==\n-----END PUBLIC KEY-----",
      "action": "SAVE"
    }
  }
}' https://${symphony.url}:443/pod/v2/admin/user/68719476742/update
{
  "userAttributes": {
    "emailAddress": "demo-bot1@symphony.com",
    "userName": "demo-bot1",
    "displayName": "DemoBot1",
    "companyName": "pod1",
    "accountType": "SYSTEM",
    "currentKey": {
      "key": "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhk...ghUGWMCAwEAAQ==\n-----END PUBLIC KEY-----"
    },
    "previousKey": {
      "key": "-----BEGIN PUBLIC KEY-----MIICIjANBgkqhki...hUGWMCAwEAAQ==-----END PUBLIC KEY-----",
      "expirationDate": 1522675669714
    }
  },
  "userSystemInfo": {
    "id": 68719476742,
    "status": "ENABLED",
    "createdDate": 1522318499000,
    "createdBy": "68719476737",
    "lastUpdatedDate": 1522416469717,
    "lastLoginDate": 1522416465367
  },
  "roles": [
    "INDIVIDUAL"
  ]
}

Additionally you can programmatically revoke a public key using either currentKey or previousKey. Use the following REST request to programmatically revoke a public key using currentKey:

curl -H 'sessionToken: eyJhbGciOiJSUzUxMiJ9...O3iq8OEkcnvvMFKg' -d '{
  {
    "currentKey": {"action":"REVOKE"}
  }
}' https://localhost.symphony.com:443/pod/v2/admin/user/68719476742/update
{
  "userAttributes": {
    "emailAddress": "bot.user1@localhost.symphony.com",
    "userName": "bot.user1",
    "displayName": "Local Bot01",
    "companyName": "pod1",
    "accountType": "SYSTEM"
  },
  "userSystemInfo": {
    "id": 68719476742,
    "status": "ENABLED",
    "createdDate": 1522318499000,
    "createdBy": "68719476737",
    "lastUpdatedDate": 1522416469717,
    "lastLoginDate": 1522416465367
  },
  "roles": [
    "INDIVIDUAL"
  ]
}

Use the following REST request to programmatically revoke a public key using previousKey:

curl -H 'sessionToken: eyJhbGciOiJSUzUxMiJ9...O3iq8OEkcnvvMFKg' -d '{
  {
    "previousKey": {"action":"REVOKE"} 
  }
}' https://localhost.symphony.com:443/pod/v2/admin/user/68719476742/update
{
  "userAttributes": {
    "emailAddress": "demo-bot1@symphony.com",
    "userName": "demo-bot1",
    "displayName": "DemoBot1",
    "companyName": "pod1",
    "accountType": "SYSTEM",
    "currentKey": {
      "key": "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhk...ghUGWMCAwEAAQ==\n-----END PUBLIC KEY-----"
    }
  },
  "userSystemInfo": {
    "id": 68719476742,
    "status": "ENABLED",
    "createdDate": 1522318499000,
    "createdBy": "68719476737",
    "lastUpdatedDate": 1522416469717,
    "lastLoginDate": 1522416465367
  },
  "roles": [
    "INDIVIDUAL"
  ]
}

Extending a Public Key

Use the following REST request to programmatically extend a public key:

curl -H 'sessionToken: eyJhbGciOiJSUzUxMiJ9...O3iq8OEkcnvvMFKg' -d '{
  {
    "previousKey": { "action": "EXTEND" } 
  }
}' https://localhost.symphony.com:443/pod/v2/admin/user/68719476742/update
{
  "userAttributes": {
    "emailAddress": "demo-bot1symphony.com",
    "userName": "demo-bot1",
    "displayName": "DemoBot1",
    "companyName": "pod1",
    "accountType": "SYSTEM",
    "currentKey": {
      "key": "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhk...ghUGWMCAwEAAQ==\n-----END PUBLIC KEY-----"
    },
    "previousKey": {
      "key": "-----BEGIN PUBLIC KEY-----MIICIjANBgkqhki...hUGWMCAwEAAQ==-----END PUBLIC KEY-----",
      "expirationDate": 1522675669714
    }
  },
  "userSystemInfo": {
    "id": 68719476742,
    "status": "ENABLED",
    "createdDate": 1522318499000,
    "createdBy": "68719476737",
    "lastUpdatedDate": 1522416469717,
    "lastLoginDate": 1522416465367
  },
  "roles": [
    "INDIVIDUAL"
  ]
}

Restricted Key Operations:

You CANNOT perform the following actions:

  • EXTEND on the current active key

  • EXTEND over an expired key

  • EXTEND over the previous key when no previous key is set for the user

  • EXTEND when no expiration date is set for the user's previous key

  • REVOKE over the previous key when no previous key is set for the user

  • REVOKE over a current key when no current key is set for the user

  • SAVE when the user already has a valid current and previous key set

  • SAVE over an old key

Note: When performing a SAVE, the key must be different from your current key.

MessageML

This article gives an overview of the message workflow and shows how message representations are used throughout the workflow. In its subpages are presented the specifications of messageML.

Introduction of the MessageML

When calling API methods that create messages, the content of the message must be sent using MessageML markup. MessageML is a tag-based language that is a subset of XHTML, with the addition of tags specific to Symphony Messaging for embedding information (e.g. a mention) into a message.

You can find the specifications of the MessageML language in the attached subpages. Even if they are grouped in different categories for documentation clarity purposes, please note you can do your own mix, respecting the specific rules explained at each component specification level.

Also, please note that messages in MessageML markup are enclosed in a <messageML> tag.

MessageML has full unicode support and messages should be sent using UTF-8 character encoding.

Note: MessageML is just a subset of PresentationML that makes it easier to construct messages from your bot. The API can ingest either MessageML or PresentationML. However, the API will only deliver messages as PresentationML to a bot.

XML Formatting

MessageML is formatted as XML and should have all tags properly formatted. For example, rather than using <br> you must use <br/>. For string attributes, standard rules for escaping XML special characters apply, i.e. escaping:

  • ' with &apos; (if single quotes are used to quote the value)

  • " with &quot; (if single quotes are used to quote the value)

  • < with &lt;

  • & with &amp; Other XML named entity sequences such as &gt; may be used.

Valid characters for hashtags and cashtags

Keywords may only contain alphanumeric characters, underscore, dot and dash.

Important: when sending numeric $cashtags as signals, add an * after the $ sign, for example, $_122450. <cash tag="$_122450"/>

Tables

Tags

MessageML supports the following tags to arrange information within a message using tables:

Tags

Description

Optional attributes

<table> <tr> <td>text</td> </tr> </table>

Render "text" in a table format.

• class • rowspan • colspan

<thead>, <tbody>, <tfoot>

Table sections.

• class

Examples

Here after you can find an example of a message sent by a bot and containing table tags as well as the structure of the messageML sent:

Text formatting and semantics

Tags

MessageML supports the following tags for formatting content within a message:

Tag

Description

Optional attributes

<br/>

Insert a line break.

None.

<a href="url"> Link Text </a>

Insert a hyperlink that will be displayed in the message.

<b>text</b>

Bold formatting. Note: when receiving a message from an Agent that contains whitespace between the last character in a bolded section and the closing </b> tag, the bold section will be returned in Markdown (i.e. surrounded by double '*' characters) instead of XHTML tags.

<i>text</i>

Italics formatting. Note: when receiving a message from an Agent that contains whitespace between the last character in an italics-formatted section and the closing </i> tag, the italics section will be returned in Markdown (i.e. surrounded by single '*' characters) instead of XHTML tags.

<pre> preformatted text </pre>

Preformatted text.

<span>text</span>

<code>code sample</code>

Format the text as a block of code.

  • language: Use the language attribute to benefit from language specific code highlighting. The following languages are available: c, cpp, csharp, css, html, java, js, jsx, php, python, r, typescript, tsx, plaintext, yaml, scala, json, shell, markdown

  • Note: language attribute is only available with Agent 20.14+.

Examples

Here after you can find an example of a message sent by a bot and containing these formatting and semantics tags as well as the structure of the messageML sent:

Versions and Compatibility

Main features introduced

Agent needed to parse message sent by the bot

Client 2.0 release

Backward client-compatibility behavior (e.g. external rooms)

Client 1.5 release

Initial release

Since the first version

Since the first version

-

Since the first version

Closing <b> and <i> without line breaks*

Since the first version

Since the first version

-

1.53

Support of the language attribute on <code> blocks.

Agent 20.14

Since the first version

-

Messages

Message API endpoints

Bots in Symphony Messaging can use the following API endpoints to create, update and suppress messages in Symphony Messaging chats:

Please see below a quick animation showing a simple lifecycle of a bot message in Symphony Messaging with a message created by the bot, updated three times, and then finally suppressed.

Overview of the Message Flow

In the Symphony Messaging message flow, messages are represented in the following markup language forms:

  • PresentationML: MessageML translated into the equivalent XHTML tags so it can be rendered and processed by any HTML client.

  • ExtensionML: PresentationML translated to a special markup for use by a front end app to perform custom rendering of an entity.

Note: MessageML is just a subset of PresentationML that makes it easier to construct messages from your bot. The API can ingest either MessageML or PresentationML. However, the API will only deliver messages as PresentationML to a bot.

MessageML Render bot

The MessageML Render bot is a productivity tool that helps developers to create chat messages compliant with Symphony Messaging’s message markup language. Using the Render bot, you can get an overview of our message format capabilities, and quickly iterate on the format of a message while previewing the display.

The Render bot replaces the Presentation ML Live Renderer Tool website, which is no longer available.

Diagram of Symphony Messaging Message Flow:

The above diagram shows the following:

  1. The Agent API encrypts the messages, and converts them to PresentationML where they are stored in Symphony Messaging 's data store.

  2. When Bot's retrieve messages via the API, the messages are delivered as PresentationML.

Message Identifiers:

Each message in Symphony Messaging has a unique message ID.

To find the message ID:

  • In the Symphony Messaging web or desktop client, click the message timestamp. The Message Status module overlay opens. The message ID is shown in the overlay footer:

  • When a message is created via the API, a messageID is returned in the response.

Encoding:

  • The message ID in the UI is in standard Base64 encoding.

  • Message IDs returned in API responses are in URL Safe Base64 encoding.

  • A message ID used in a URL should be in URL Safe Base64 encoding.

  • To obtain the URL Safe Base64 conversation (stream) ID, replace forward slashes with underscores, replace pluses with minuses, and ignore any trailing equal signs.

Message Size Limits

Messages may include:

  • Maximum 80 entities (tags and mentions).

  • Maximum 2,500 unique tokens (distinct words) in the markdown representation of the message.

  • 81,130 characters of the encrypted markdown representation of the message. This corresponds approximately to 60,000 characters before encryption. Note that there is a greater chance of reaching the token or the entity limit than the character limit.

  • The size of a message cannot exceed 1.5Mb, including both the messageML (message property) and the entityJSON (data property)

Message Delivery for External Rooms (Cross-Pod)

Special Characters

Tags

The Apache Freemarker uses the HTML output format by default. In some cases, special characters placed within the MessageML must be HTML-escaped, otherwise, the request sending the MessageML will receive a 400 error response. The following are examples of valid HTML-escaping:

Examples

Here after you can find an example of a message sent by a bot and containing these special characters as well as the structure of the messageML sent:

Images

Tags

MessageML supports the following tags to embed media into messages:

Rules and Limitations via an example

For the following examples, we are sending an SVG image along with the message.

Note that an admin user might have to enable the sending of some specific file types. To do that, go to AC Portal >> Company Settings >> Edit Entitlements >> File Types.

Limit on Image size for Data URL (base64)

This feature is intended to be used for small images, such as custom emoji. Our recommendation is that the total size of base64 encoded embedded images do not exceed 25KB per message.

Examples

Here after you can find an example of a message sent by a bot and containing an inline image as well as the structure of the messageML sent:

Content Grouping

Tags

MessageML supports the following tags for grouping information within a message:

Examples

Here after you can find an example of a message sent by a bot and containing these content grouping tags as well as the structure of the messageML sent:

Please note that you can use content grouping tags mixed with other messageML tags such as Interactive Elements Forms. See the following example including forms in expandable-cards.

Real-Time Events

The following events are returned when reading from a real time messages and events from the datafeed:

All events follow the structure below:

  • id: A randomly generated ID for the event, distinct from the ID of the message or chatroom or other objects that the event pertains to. These IDs are unique, except in the situation where two activities are performed simultaneously in the Symphony Messaging client (for example, members are added during the creation of a room). In this case, the two associated events will share the same id. This is for Symphony Messaging internal use.

  • timestamp: The timestamp for when the event occurred in Unix timestamp milliseconds format. For certain events, this will be equivalent to the created timestamp of the object the event pertains to (for instance, room creation timestamp).

  • type: The type of event that occurred (for instance, a "MessageSent" or "RoomCreated" event).

  • initiator: The actor that initiated the event. Currently, initiator will always be a user. The initiator user may be distinct from the affected user (for instance, when one user adds another user to a chatroom).

  • payload: An object containing event-specific information that is keyed off the event type.

Message Sent

Generated when a message is sent in an 1-1 chat or chatroom of which the user in context is a member, including messages sent by the user him/herself.

Events attributes details:

  • version: version of the message payload, set when the message was created

  • message: the message itself

  • attachments: list of attachments to the message, not present if there are no attachments

  • user: author of the message

  • externalRecipients: indicates whether that message is sent to external users.

    • false: the message was only sent to users internal to the company.

    • true: the message was sent to at least one user outside of the company.

  • stream: stream the message was posted into.

    • streamId: identifier for the stream.

    • streamType: stream type can be ROOM, IM (1-1 chat), MIM (deprecated), or POST.

  • data: JSON structure contained inside an escaped string, NOT an actual JSON structure.

Note:

  • The event initiator and the message user are the same

  • Hashtags and cashtags in EntityJSON have no '#' or '$'

  • An external chatroom can contain messages with externalRecipients set to false in cases where external users have not been added to the chatroom yet.

Messages Suppressed

Generated when messages are suppressed:

Symphony Messaging Elements Action

Note that since Symphony v20.3.1, the event payload returned by the Datafeed has changed. The attribute actionStream has been removed and the formStream attribute has been renamed to stream.

Shared Wall Posts

Generated when either:

  • The user in context shares a wall post written by another user.

  • Another user shares a wall post written by the user in context.

IM Created

Generated when an IM (1-1 chat) is created with the user in context as a member, initiated either by the user in context or another user:

Room Created

Generated when a room is created by the user in context:

Room Updated Message

Generated when a room of which the user in context is a member is updated, including rooms updated by the user him/herself:

Room Deactivated Message

Generated when a room of which the user in context is a member is deactivated, including rooms deactivated by the user him/herself:

The ROOMDEACTIVATED event does not include the roomName or external fields.

Room Reactivated Message

Generated when a room of which the user in context is a member is reactivated, including rooms reactivated by the user him/herself:

User Requested to Join Room

Generated when a user requests to join a room. Only the user who requested to join the room and the owners of that room will receive this event on their datafeeds. The affectedUsers attribute represents the owners of the room. Available in Agent 2.56.0.

User Joined Room

Generated when a new user joins or is added to a room of which the user in context is a member, including when the user himself joins or is added to a room.:

User Left Room

Generated when a user leaves or is removed from a room of which the user in context is a member, including when the user himself leaves or is removed from a room:

Room Member Promoted to Owner

Generated when a user is promoted from a participant to an owner of a room of which the user in context is a member, including when the user himself is promoted to an owner or promotes another user.:

Room Member Demoted from Owner

Generated when a user is demoted from an owner to a participant of a room of which the user in context is a member, including when the user himself is demoted to a participant or demotes another user:

Connection Requested

Generated when a connection request is sent, either:

  • Sent by the user in context to another user.

  • Sent to the user in context by another user.

Connection Accepted

Generated when a connection request is accepted, either:

  • Sent by the user in context and accepted by another user.

  • Sent by another user and accepted by the user in context.

Generic System Event

Generic System Event is a new category of generic real time event, that is generated in various situations, such as phone calls, events specific to federated chats, and more.

The event is generic, and its structure depends on the event's subtype (genericSystemEvent.eventSubtype).

Most generic events are related to internal processes and are not useful for Bots, so you can just ignore them. When Symphony introduces events that can be relevant to Bots, they will be documented in this page.

The common structure to all Generic System Events is described below. The parameters section will vary depending on the subtype of the event.

Always rely on the eventSubtype to filter the events that are relevant to you, as new subtypes with a different structure may be introduced in the future.

Style Attributes

Attributes supported in messageML tags

Tags support the following style attributes where applicable:

Go further...

To learn more about Symphony's built in styles, continue here:

Tags and mentions

Tags

MessageML supports the following tags to embed additional information into messages:

Examples

Here after you can find an example of a message sent by a bot and containing these tags specific to Symphony Messaging as well as the structure of the messageML sent:

Mentions

Free-text Tags

Enhanced tags (Financial instruments)

Below several examples of financial instruments, using different types of identifiers and filters.

When identifiers and filters are not sufficient to identify a unique match, or when an instrument is not found in our reference database, an error is returned, except if a fallback-ticker is specified.

Chime

Enhanced tags notice

This page describes the changes introduced by the new enhanced tags for Symphony developers

Introduction to the enhanced tags

Symphony users can now tag financial instruments using the Enhanced Tag feature.

It displays a typeahead when the user presses # in a chat conversation. The upper section of the typeahead shows regular hashtags previously exchanged with that user and the lower section, under INSTRUMENTS shows Stocks, ADRs, ETFs and Indices.

By hovering over the Enhanced Tag, users can see essential reference data like company name, identifiers, exchange and currency.

The enhanced tags replace the existing $cashtags. This means that moving forward your apps and bots will need to handle enhanced tags instead of $cashtags.

Understanding the impact of the new tags

The impact of the enhanced tags on existing bots & apps has been carefully reviewed to ensure that there is no backward compatibility issue. However, we strongly recommend that you conduct some tests to validate the change.

Please read below to understand the changes introduced with the new enhanced tags.

Bots producing legacy $cashtags

  • Existing bots can still produce legacy $cashtags both using the short tag notation (<tag cash="AAPL"/>), and using a Standard Entity ("type": "org.symphonyoss.fin.security")

Bots consuming legacy $cashtags

  • Bots consuming existing $cashtags in received messages are not impacted if they rely on the structured object (entityJSON). When they receive an enhanced tag, the exact same structure and properties will be present. There will be additional properties though, and the version field will be set to "2.0" instead of "1.0".

  • Bots that rely on the text representation of the tags (message field instead of the entityJSON field) will also not be impacted. However, this is not a recommended practice as this text representation may change to match the enhanced tag displayed symbol instead of the existing ticker in the future (e.g., text would change from "$TSLA" to "TSLA US"). If you rely on the text representation of the $cashtag instead of the structured data, you should consider updating the bot to rely on the structured data included in the entityJSON instead.

  • If you plan to upgrade your bot to leverage the new properties of the enhanced tags, please consider that you may still encounter legacy $cashtags for a long period of time, so should build around that. To detect if you are receiving a legacy tag or an enhanced one, you can rely on the version field included in the structure (org.symphonyoss.fin.security.version). It is set to "2.0" for the new enhanced tags.

Signals

  • Existing signals will continue to work with the new enhanced tags. Messages containing the new tags will still match with the existing signals, meaning a signal matching "cash: AAPL" will match messages containing both "AAPL" and "AAPL US" for example.

  • For now, it is not yet possible to create signals based on the newly introduced properties of the enhanced tags, but this will change in the coming months. For example, it is not yet possible to create a signal that will match only on "TSLA US", but if you create a signal that matches on "TSLA", it will match messages containing "TSLA US" as well.

  • The Signal API has not changed. In the coming months the API may change to adapt to the new tags and in that case this change will be introduced following a notice period provided that there is an impact on the API contract.

Extension apps

  • Existing apps that register a UI extension on $cashtag hovercards continue to receive the existing object for new enhanced tags.

  • However, whenever your app is notified of a user click on the hovercard, the app will receive in addition to the existing object a new structure containing the new enhanced tags properties.

  • If you decide to upgrade your app to leverage the new properties of the enhanced tags, please consider that you may still encounter legacy $cashtags for a long period of time, so should build around that. To detect if you are receiving a legacy tag or an enhanced one, you can rely on the version field included in the structure (org.symphonyoss.fin.security.version). It is set to "2.0" for the new enhanced tags.

Message format

Please note below the differences between the structures of the enhanced tags and the legacy $cashtags.

Structure of legacy $cashtags

Structure of enhanced tags

Symphony Messaging Agent APIs

Message APIs

The full list of Messages API endpoints and their corresponding reference guide can be found here:

Datafeed APIs

The full list of Datafeed API endpoints and their corresponding reference guide can be found here:

Signal APIs

The full list of Signals API endpoints and their corresponding reference guide can be found here:

Basic APIs

The full list of Basics API endpoints and their corresponding reference guide can be found here:

Session Auth:

Key Manager Auth:

survive session expiration, you do not need to re-create your datafeed if your session expires.

A JWT payload has to be signed with RS512 ()"

The authentication token can be inspected on or .

When or messages using the API, MessageML shorthand tags are translated into equivalent XHTML tags and returned in .

<messageML>
    <h1>Tables</h1>
    <p>This is table:</p>
    <table>
        <thead>
            <tr>
                <td>Header column 1</td>
                <td>Header column 2</td>
                <td>Header column 2</td>
            </tr>
        </thead>
        <tbody>
            <tr>
                <td>Cell row 1 column 1</td>
                <td>Cell row 1 column 2</td>
                <td>Cell row 1 column 3</td>
            </tr>
            <tr>
                <td>Cell row 2 column 1</td>
                <td>Cell row 2 column 2</td>
                <td>Cell row 2 column 3</td>
            </tr>
        </tbody>
        <tfoot>
            <tr>
                <td>Footer column 1</td>
                <td>Footer column 2</td>
                <td>Footer column 2</td>
            </tr>
        </tfoot>
    </table>
</messageML>

• href: the URL of the link • class: .

• class: .

• class: .

• class: . • Non-HTML MessageML are not supported inside <pre>.

No formatting. • This tag can be used to specify visual styles, by adding a class attribute. • This tag is used to create .

• class: . • data-entity-id • See below for list of translated PresentationML attributes.

Note: please see escaping rules for when using code tag

<messageML>
    <h1>Formatting and semantics</h1>
    <p>This is a <a href="https://docs.developers.symphony.com/">Link</a> to our developer documentation.</p>
    <p>Part 1 of the paragraph separated from part 2 with a line break.<br/>Part 2 of the paragraph.</p>
    <p>This text can be either <b>bold</b> or <i>italic</i> or <span style="color:red;">red</span>, or even combine <b><i style="color:red;">several</i></b> formatting tags.</p>
    <pre>This is a preformatted text.</pre>
    <code language="html">&lt;h1&gt;Code sample&lt;/h1&gt;
&lt;p&gt;This is some html&lt;/p&gt;</code>
</messageML>

Message creation via the endpoint

Message creation in multiple conversations using the endpoint

Message update via the endpoint. Note this functionality is not supported on Mobile. On Mobile, updates will appear as new messages instead of replacing the actual message. Also, note the rules and limitations explained in the endpoint specifications documented .

Message suppression via the endpoint

MessageML: A tag-based language that is a subset of XHTML. MessageML allows templating in .

The Render bot is available . If this link doesn't work, you can look for MessageML Render bot in the directory.

Your bot uses the to send messages in MessageML.

The delivers messages to end users or to the Desktop Application as PresentationML.

In the Symphony Messaging architecture, messages are stored in each company data store. When messages are sent externally (for example in External rooms), the messages are copied and transferred from one company to the other using the Amazon AWS SQS service . Amazon AWS SQS service ensures high availability through redundancy, with "At-least-once delivery" guarantee. This means that on very rare occasions, a message can be received more than once.

If this occurs, you might receive a second copy of that message. Therefore, Bots designed to work with External rooms (cross pod) should be idempotent: they should not be affected adversely when processing the same message more than once. Continue to learn about how your bot should handle duplicate messages.

• Sending an image via the API using the image URL

Sending an image via API using Data URL (base64 encoding). Note that it is necessary to include data:image/imageType+xml;base64 before the data string, as shown in the following example:

Generated when a user replies to a bot message that contains an interactive form with UX components such as text fields, radio buttons, checkboxes, person selectors and more. Please refer to for more information.

Existing bots can easily include enhanced tags in messageML now. To do so, you will need to cuse the <tag> and provide one or several identifiers for that instrument. The instrument will then be resolved against our reference database, and properly formatted in the message. More information is available .

https://developers.symphony.com/restapi/main/messages
Datafeed
https://developers.symphony.com/restapi/main/datafeed
https://developers.symphony.com/restapi/main/signals
https://developers.symphony.com/restapi/main/info-health-check
https://developers.symphony.com/restapi/main/bot-authentication/rsa-session-authenticate
https://developers.symphony.com/restapi/main/bot-authentication/rsa-key-manager-authenticate
Configure your Bot for BDK 2.0 for Java
Datafeeds
https://tools.ietf.org/html/rfc7518#section-3.1
https://jwt.io/
https://www.jsonwebtoken.io/
creating
retrieving
PresentationML

StreamID

URL Safe Base64 encoded StreamID

RUkxW4x40aB74g0UWpaMw3///ozLPsapdA==

RUkxW4x40aB74g0UWpaMw3___ozLPsapdA

Example of payload if the message exceeds the total size limit (default being 1.5Mb)
{
message=Limit message-content-max-total-size reached, 
status=PAYLOAD_TOO_LARGE, 
code=LIMIT_MESSAGE_CONTENT_MAX_TOTAL_SIZE_REACHED
}

Character

HTML escaping

Required escaping

messageML example

<

&lt;

Yes

<messageML>&lt;</messageML>

&

&#38;

Yes

<messageML>&#38; </messageML>

$

&#36;

Yes The $ character only needs to be escaped if it comes before the { character.

<messageML>&#36;{}</messageML>

#

&#35;

Yes The # character only needs to be escaped if it comes before the { character.

<messageML>&#35;{}</messageML>

>

&gt;

No

<messageML>&gt;</messageML>

"

&quot;

No

<messageML>&quot;</messageML>

'

&#39;

No

<messageML>&#39;</messageML>

*

&#42;

No

<messageML>&#42;</messageML>

%

&#37;

No

<messageML>&#37;</messageML>

$ curl -X POST https://yourpod.com/agent/v4/stream/:sid/message/create \
-H 'content-type: multipart/form-data' \
-H 'keyManagerToken: 01008e3fc538...5d82870985576' \
-H 'sessionToken: eyJhbGciOiJSU...6YmlWyim0peFkMA' \
-F 'message=<messageML>Sending attachment via API<img src="https://yourimg.com/test/myimage.svg"></img></messageML>'
$ curl -X POST https://yourpod.com/agent/v4/stream/:sid/message/create \
-H 'content-type: multipart/form-data' \
-H 'keyManagerToken: 01008e3fc538...5d82870985576' \
-H 'sessionToken: eyJhbGciOiJSU...6YmlWyim0peFkMA' \
-F 'message=<messageML>Sending attachment via API<img src="data:image/svg+xml;base64,PHN2ZyBpZD0i...DcuMjcsMTYuN="></img></messageML>'
<messageML>
    <h1>Images</h1>
    <p>This is an image: <img src="https://play-lh.googleusercontent.com/-lTkJjeUtCj3Mf4FLLNKnCyQC0Amur3wxeKxkwbDZl0hjO60H1_VodAuBDKJmYWOvlfG=s180-rw" /></p>
</messageML>
<messageML>
  <form id="form_id">
    <h1>expandable-cards</h1>
      <h2>1st expandable-card</h2>
      <expandable-card state="collapsed">
        <header><h3>Header Content (always visible)</h3>This is a <b>collapsed</b> expandable-card with the <b>default</b> variant</header>
        <body>
          <h3>Body Content</h3>
          It is possible to use Elements in the body as you can see below:
          <text-field name="id1" placeholder="Enter your name..."/>
          <text-field name="id2" placeholder="Enter your age..."/>
          <button type="reset">Reset</button>
        </body>
      </expandable-card><br/>
      <h2>2nd expandable-card</h2>
      <expandable-card state="expanded">
        <header><h3>Header Content (always visible)</h3><text-field name="id_header" label="Text-field in the header" placeholder="Type your input..."/>This is an <b>expanded</b> expandable-card with the <b>default</b> variant and with a <b>Symphony Element</b> in the header</header>
        <body>
          <h3>Body Content</h3>
          When expanded, the whole body content is displayed in the expandable-card.
          <text-field name="id3" placeholder="Enter your name..."/>
          <text-field name="id4" placeholder="Enter your age..."/>
        </body>
      </expandable-card><br/>
      <h2>3rd expandable-card</h2>
      <expandable-card state="cropped">
        <header><h3>Header Content (always visible)</h3>This is a <b>cropped</b> expandable-card with the <b>error</b> variant</header>
        <body variant="error">
          <h3>Body Content</h3>
          When cropped, the body content is displayed in the expandable-card until a certain limit. A show more button allows to expand the card and display the rest of the body.
        </body>
      </expandable-card>
    <button name="form_id_submit">Submit</button>
  </form>
</messageML>
{
    "id": "",
    "timestamp": 0,
    "type": "MESSAGESENT",
    "initiator": {
        "user": {
            "userId": 0,
            "firstName": "",
            "lastName": "",
            "displayName": "",
            "email": "",
            "userName": ""
        }
    },
    "payload": {
        "messageSent": {
            "message": {
                "messageId": "",
                "timestamp": 0,
                "message": "<div data-format='PresentationML' data-version='2.0'>Message content. With <span class='entity' data-entity-id='mention1'>@user name</span>, <span class='cashTag' data-entity-id='cashtag2'>$ticker</span>, and <span class='hashTag' data-entity-id='hashtag3'>#keyword</span>.</div>",
                "data": "{
          \"mention1\": {
            \"type\": \"com.symphony.user.mention\",
            \"version\": \"1.0\",
            \"id\": [{
              \"type\": \"com.symphony.user.userId\",
              \"value\": 123456789
            }]
          },
          \"cashtag2\": {
            \"type\": \"org.symphonyoss.fin.security\",
            \"version\": \"1.0\",
            \"id\": [{
              \"type\": \"org.symphonyoss.fin.security.id.ticker\",
              \"value\": \"ibm\"
            }]
          },
          \"hashtag3\": {
            \"type\": \"org.symphonyoss.taxonomy\",
            \"version\": \"1.0\",
            \"id\": [{
              \"type\": \"org.symphonyoss.taxonomy.hashtag\",
              \"value\": \"unexpected\"
            }]
          }
        }",
                "attachments": [],
                "user": {
                    "userId": 0,
                    "firstName": "",
                    "lastName": "",
                    "displayName": "",
                    "email": "",
                    "username": ""
                },
                "externalRecipients": false,
                "stream": {
                    "streamId": "",
                    "streamType": "IM"
                },
                "userAgent": "Agent-2.2.5-Linux-4.9.77-31.58.amzn1.x86_64",
                "originalFormat": "com.symphony.messageml.v2"
            }
        }
    }
}
{
      "id": "V2Jdlz",
      "messageId": "qsOkkR1Z170QKVu54QU8WX___qRaa2y5bw",
      "timestamp": 1493131629382,
      "type": "MESSAGESUPPRESSED",
      "initiator": {
                "user": {
                    "userId": 8933531976380
                    }
            },
            "payload": {
                "messageSuppressed": {
                    "messageId": "QasBvaApRVqAIGPcqUTOAn___qRaa6hwdA",
                        "stream": {
                            "streamId": "ksuDd0xGnAGW1B4plItFyn___qRfGLBsdA"
                        }
                    }
            }
}
[
    {
        "id": "HHZFPX",
        "messageId": "StQDPAYMXNz1ue4fcr5W7H___pQENqrGbQ",
        "timestamp": 1563297404217,
        "type": "SYMPHONYELEMENTSACTION",
        "initiator": {
            "user": {
                "userId": 7078106482890,
                "firstName": "User",
                "lastName": "Bot",
                "displayName": "User",
                "email": "User_bot@symphony.com",
                "username": "user_bot"
            }
        },
        "payload": {
            "symphonyElementsAction": {
                "stream": {
                    "streamId": "YuK1c2y2yuie6+UfQnjSPX///pQEn69idA=="
                },
                "formMessageId": "xtZqqBNGwLDkLuvuQTyjH3///pQENvjudA==5454",
                "formId": "form_id",
                "formValues": {
                    "action": "submit_button",
                    "name_01": "John",
                    "email_01": "john@email.com",
                    "country": "opt1",
                    "example_radio": "option_01",
                    "checkbox_1": "value01",
                    "checkbox_2": "value02",
                    "comment": "my comment"
                }
            }
        }
    }
]
{
  "id": "",
  "timestamp": 0,
  "type": "SHAREDPOST",
  "initiator": {
    "user": {
      "userId": 0,
      "firstName": "",
      "lastName": "",
      "displayName": "",
      "email": "",
      "username": ""
    }
  },
  "payload": {
    "sharedPost": {
      "message": {
        "messageId": "",
        "timestamp": 0,
        "message": "<div data-format='PresentationML' data-version='2.0'>Sent when sharing the original post</div>",
        "data" : "{ }",
        "attachments": [],
        // This is the user who is sharing the wall post.
        "user": {
          "userId": 0,
          "firstName": "",
          "lastName": "",
          "displayName": "",
          "email": "",
          "username": ""
        },
        "externalRecipients": false,
        "stream": {
          // This is the ID of the sharing user's wall.
          "streamId": "",
          // For a shared wall post, the destination stream type will always be POST.
          "streamType": "POST"
        }
      },
      // This is the original wall post that is being shared.
      "sharedMessage": {
        "messageId": "",
        "timestamp": 0,
        "message": "<div data-format='PresentationML' data-version='2.0'>Original Post</div>",,
        "data" : "{}",
        "attachments": [],
        // This is the author of the original wall post.
        "user": {
          "userId": 0,
          "firstName": "",
          "lastName": "",
          "displayName": "",
          "email": "",
          "username": ""
        },
        "externalRecipients": false,
        "stream": {
          // This is the ID of the original poster's wall.
          "streamId": "",
          // For a shared wall post, the originating stream type will always be POST.
          "streamType": "POST"
        }
      }
    }
  }
}
{
  "id": "",
  "timestamp": 0,
  "type": "INSTANTMESSAGECREATED",
  "initiator": {
    "user": {
        "userId": 0,
      "firstName": "",
      "lastName": "",
      "displayName": "",
      "email": "",
      "username": ""
    }
  },
  "payload": {
    "instantMessageCreated": {
      "stream": {
        "streamId": "",
        // Stream type can be either IM (1-1 chat) or MIM (Deprecated).
        "streamType": "IM",
        "members": [
          { "userId": 0 },
          { "userId": 1 }
        ],
        "external": false
      }
    }
  }
}
[
    {
        "id": "uW6hmX",
        "messageId": "nn-TBGB...",
        "timestamp": 1535146379937,
        "type": "ROOMCREATED",
        "initiator": {
            "user": {
                "userId": 14568...,
                "displayName": "Local Bot",
                "email": "bot.user@test.symphony.com",
                "username": "bot.user"
            }
        },
        "payload": {
            "roomCreated": {
                "stream": {
                    "streamId": "tVtJmEG...",
                    "streamType": "ROOM",
                    "roomName": "pub12",
                    "external": false,
                    "crossPod": false
                },
                "roomProperties": {
                    "name": "pub12",
                    "description": "#twelfth",
                    "creatorUser": {
                        "userId": 14568...,
                        "displayName": "Local Bot",
                        "email": "bot.user@test.symphony.com",
                        "username": "bot.user"
                    },
                    "createdDate": 1535146379931,
                    "external": false,
                    "crossPod": false,
                    "copyProtected": false,
                    "readOnly": false,
                    "discoverable": false,
                    "membersCanInvite": false,
                    "canViewHistory": false,
                    "public": true
                }
            }
        }
    }
]
{
    "id": "",
  "timestamp": 0,
  "type": "ROOMUPDATED",
  "initiator": {
    "user": {
        "userId": 0,
      "firstName": "",
      "lastName": "",
      "displayName": "",
      "email": "",
      "username": ""
    }
  },
  "payload": {
    "roomUpdated": {
        "stream": {
        "streamId": "",
        "streamType": "ROOM",
        "roomName": "",
        "external": false
      },
      // These fields represent the current state of these attributes. At least one of them was updated by this operation.
      "newRoomProperties": {
        "name": "",
        "description": "",
        "discoverable": false,
        "membersCanInvite": false
      }
    }
  }
}
{
    "id": "",
  "timestamp": 0,
  "type": "ROOMDEACTIVATED",
  "initiator": {
    "user": {
        "userId": 0,
      "firstName": "",
      "lastName": "",
      "displayName": "",
      "email": "",
      "username": ""
    }
  },
  "payload": {
    "roomDeactivated": {
        "stream": {
        "streamId": "",
        "streamType": "ROOM"
      }
    }
  }
}
{
    "id": "",
  "timestamp": 0,
  "type": "ROOMREACTIVATED",
  "initiator": {
    "user": {
        "userId": 0,
      "firstName": "",
      "lastName": "",
      "displayName": "",
      "email": "",
      "username": ""
    }
  },
  "payload": {
    "roomReactivated": {
        "stream": {
        "streamId": "",
        "streamType": "ROOM",
        "roomName": "",
        "external": false
      }
    }
  }
}
[
    {
        "id": "LSWslw",
        "messageId": "lwu0jF0reb7Sbw",
        "timestamp": 1574439227693,
        "type": "USERREQUESTEDTOJOINROOM",
        // This is the user who initiated the action, in this case, the user who has requested to join the room. 
        "initiator": {
            "user": {
                "userId": 68719476737,
                "firstName": "John",
                "lastName": "Doe",
                "displayName": "John Doe",
                "email": "john_doe@symphony.com",
                "username": "john_doe@symphony.com"
            }
        },
        "payload": {
            "userRequestedToJoinRoom": {
                "stream": {
                    "streamId": "cVHHJfFJbjyQ4bmHsHJBcdA",
                    "members": []
                },
               //These are the users who own the room.
                "affectedUsers": [
                    {
                        "userId": 68719476759,
                        "displayName": "owner1",
                        "email": "owner1@email.com",
                        "username": "owner1"
                    },
                    {
                        "userId": 68719476760,
                        "firstName": "owner2",
                        "lastName": "owner2",
                        "displayName": "owner2 owner2",
                        "email": "owner2@mail.com",
                        "username": "owner2"
                    }
                ]
            }
        }
    }
]
{
    "id": "",
  "timestamp": 0,
  "type": "USERJOINEDROOM",
  // This is the user who initiated the action (i.e. added the user to the room). If the initiator and and affected user are the same, the user joined the room of his own accord.
  "initiator": {
    "user": {
        "userId": 0,
      "firstName": "",
      "lastName": "",
      "displayName": "",
      "email": "",
      "username": ""
    }
  },
  "payload": {
    "userJoinedRoom": {
        "stream": {
        "streamId": "",
        "streamType": "ROOM",
        "roomName": "",
        "external": false
      },
      // This is the user who was affected by the action (i.e. joined the room).
      "affectedUser": {
                "userId": 0,
        "firstName": "",
        "lastName": "",
        "displayName": "",
        "email": "",
        "username": ""
      }
    }
  }
}
{
    "id": "",
  "timestamp": 0,
  "type": "USERLEFTROOM",
  // This is the user who initiated the action (i.e. removed the user from the room). If the initiator and and affected user are the same, the user left the room of his own accord.
  "initiator": {
    "user": {
        "userId": 0,
      "firstName": "",
      "lastName": "",
      "displayName": "",
      "email": "",
      "username": ""
    }
  },
  "payload": {
    "userLeftRoom": {
        "stream": {
        "streamId": "",
        "streamType": "ROOM",
        "roomName": "",
        "external": false
      },
      // This is the user who was affected by the action (i.e. left the room).
      "affectedUser": {
                "userId": 0,
        "firstName": "",
        "lastName": "",
        "displayName": "",
        "email": "",
        "username": ""
      }
    }
  }
}
{
    "id": "",
  "timestamp": 0,
  "type": "ROOMMEMBERPROMOTEDTOOWNER",
  // This is the user who performed the promotion.
  "initiator": {
    "user": {
        "userId": 0,
      "firstName": "",
      "lastName": "",
      "displayName": "",
      "email": "",
      "username": ""
    }
  },
  "payload": {
    "roomMemberPromotedToOwner": {
        "stream": {
        "streamId": "",
        "streamType": "ROOM",
        "roomName": "",
        "external": false
      },
      // This is the user who was promoted.
      "affectedUser": {
                "userId": 0,
        "firstName": "",
        "lastName": "",
        "displayName": "",
        "email": "",
        "username": ""
      }
    }
  }
}
{
    "id": "",
  "timestamp": 0,
  "type": "ROOMMEMBERDEMOTEDFROMOWNER",
  // This is the user who performed the demotion.
  "initiator": {
    "user": {
        "userId": 0,
      "firstName": "",
      "lastName": "",
      "displayName": "",
      "email": "",
      "username": ""
    }
  },
  "payload": {
    "roomMemberDemotedFromOwner": {
        "stream": {
        "streamId": "",
        "streamType": "ROOM",
        "roomName": "",
        "external": false
      },
      // This is the user who was demoted.
      "affectedUser": {
                "userId": 0,
        "firstName": "",
        "lastName": "",
        "displayName": "",
        "email": "",
        "username": ""
      }
    }
  }
}
{
    "id": "",
  "timestamp": 0,
  "type": "CONNECTIONREQUESTED",
  // This is the user who sent the request.
  "initiator": {
    "user": {
        "userId": 0,
      "firstName": "",
      "lastName": "",
      "displayName": "",
      "email": "",
      "username": ""
    }
  },
  "payload": {
    "connectionRequested": {
      // This is the user to whom the request was sent.
      "toUser": {
        "userId": 0,
        "firstName": "",
        "lastName": "",
        "displayName": ""
      }
    }
  }
}
{
    "id": "",
  "timestamp": 0,
  "type": "CONNECTIONACCEPTED",
  // This is the user who accepted the request.
  "initiator": {
    "user": {
        "userId": 0,
      "firstName": "",
      "lastName": "",
      "displayName": "",
      "email": "",
      "username": ""
    }
  },
  "payload": {
    "connectionAccepted": {
      // This is the user who sent the request.
      "fromUser": {
        "userId": 0,
        "firstName": "",
        "lastName": "",
        "displayName": "",
        "email": ""
      }
    }
  }
}
{
  "id": "chiKiF",
  "messageId": "XB1kb8IWoQJiPm7EIp-Qan___m_USNrLbw",
  "timestamp": 1718720341300,
  "type": "GENERICSYSTEMEVENT",
  "initiator": {
    "user": {
      "userId": 12345678,
      "displayName": "connect-bot",
      "email": "connect-bot@symphony.com",
      "username": "connect-bot"
    }
  },
  "payload": {
    "genericSystemEvent": {
      "stream": {
        "streamId": "_tiUL8zamhwqo8J0tRGRmH___nF1ABgsdA",
        "external": true,
        "crossPod": true
      },
      "eventTimestamp": 1718717111858,
      "sourceSystem": "federation",
      "eventSubtype": "test.event",
      "parameters": {
        ... (depends on subtype)
      }
    }
  }
}
align-content
align-items
align-self
background
background-attachment
background-blend-mode
background-clip
background-color
background-image
background-position
background-repeat
background-size
border
border-bottom
border-bottom-color
border-bottom-left-radius
border-bottom-right-radius
border-bottom-style
border-bottom-width
border-collapse
border-color
border-image
border-image-outset
border-image-repeat
border-image-slice
border-image-source
border-image-width
border-left
border-left-color
border-left-style
border-left-width
border-radius
border-right
border-right-color
border-right-style
border-right-width
border-spacing
border-style
border-top
border-top-color
border-top-left-radius
border-top-right-radius
border-top-style
border-top-width
border-width
box-shadow
box-sizing
caption-side
clear
color
content
counter-increment
counter-reset
display
empty-cells
flex
flex-basis
flex-direction
flex-flow
flex-grow
flex-shrink
flex-wrap
font
font-family
font-kerning
font-size
font-size-adjust
font-stretch
font-style
font-variant
font-weight
gap
grid
grid-area
grid-auto-columns
grid-auto-flow
grid-auto-rows
grid-column
grid-column-end
grid-column-gap
grid-column-start
grid-gap
grid-row
grid-row-end
grid-row-gap
grid-row-start
grid-template
grid-template-areas
grid-template-columns
grid-template-rows
height
justify-content
justify-items
justify-self
letter-spacing
line-height
list-style
list-style-image
list-style-position
list-style-type
margin
margin-bottom
margin-left
margin-right
margin-top
max-height
max-width
min-height
min-width
opacity
outline
outline-color
outline-offset
outline-style
outline-width
overflow
overflow-x
overflow-y
padding
padding-bottom
padding-left
padding-right
padding-top
place-content
place-items
place-self
table-layout
text-align
text-align-last
text-decoration
text-decoration-color
text-decoration-line
text-decoration-style
text-indent
text-justify
text-overflow
text-shadow
text-transform
visibility
white-space
width
word-break
word-spacing
word-wrap

Tag

Description

Optional attributes

<mention uid="123456789"/>

Insert a mention for the user whose Symphony userid is 123456789.

<mention email="user@music.org"/>

Insert a mention for the user whose email address is user@music.org.

• strict=true, the API will throw an error if no user of that email address exists. (default) • strict=false . Message is accepted even if the user cannot be resolved.

<hash tag="label"/>

Insert "label" as a free-text hashtag.

<cash tag="ticker"/>

Insert "ticker" as a free-text cashtag. Important: when sending numeric cashtags as signals, add a * after the $ sign, for example, $_122450. <messageML> `<cash tag="$_122450"/> `\

Note: Cashtags are deprecated. Please use the <tag /> notation for financial instruments.

<tag />

Insert a financial instrument (enhanced tag) in your message, coming from our reference database.

The following instruments are supported: Stocks, ETFs, Indices and currency pairs. To identify an instrument, you'll need to provide at least one identifier (e.g. an ISIN), and optionally some filters if your identifier is not specific enough. You can also specify a fallback-ticker that will act as a free-text tag (workaround) if we are not able to find the instrument referenced.

More information on the new tags is available in the Enhanced tags notice.

Identifiers:

  • fullbbgcompticker

  • figi

  • bbgcompticker

  • us-code

  • isin

  • local-code

Filters:

  • instrument-class

  • bbgmarket-sector

  • return-main-listing

  • country-code

  • operational-mic

Others:

  • fallback-ticker

<chime />

Send a chime alert.

Note: No other content (even line breaks) is permitted with a <chime/> tag. Please see an example of the messageML to send a chime below.

<messageML>
  <br/><span>Mention with an email: </span>
  <mention email="pierre.neu@symphony.com"/>
  <br/><span>Mention with a user Id: </span>
  <mention uid="71811853190567"/>
</messageML>
<messageML>
  <br/>
  <p>Hashtags: <hash tag="important"/><hash tag="stockmarket"/></p>
  <p>Legacy cashtags:	<cash tag="AAPL US"/> <cash tag="TSLA US"/></p>
</messageML>
<messageML>
  <table class="pasted-table">
    <thead>
      <tr>
        <th>Identifiers and filters</th>
        <th>Tag</th>
        <th>Comment</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>FullBBGCompTicker</td>
        <td><tag fullbbgcompticker="TSLA US Equity"/></td>
        <td># unique if found</td>
      </tr>
      <tr>
        <td>Figi</td>
        <td><tag figi="BBG000N9P426" fallback-ticker="TSLA"/></td>
        <td># unique if found</td>
      </tr>
      <tr>
        <td>BBG Comp ticker on Market sector</td>
        <td><tag bbgcompticker="TSLA US" bbgmarket-sector="Equity" fallback-ticker="TSLA"/></td>
        <td># unique if found</td>
      </tr>
      <tr>
        <td>Figi ticker</td>
        <td><tag figi-ticker="TSLA UW" fallback-ticker="TSLA"/></td>
        <td># likely unique, may need filters</td>
      </tr>
      <tr>
        <td>BBG Comp ticker</td>
        <td><tag bbgcompticker="TSLA US" fallback-ticker="TSLA"/></td>
        <td># likely unique, may need filters</td>
      </tr>
      <tr>
        <td>US Code</td>
        <td><tag us-code="88160R101" fallback-ticker="TSLA"/></td>
        <td># likely NOT unique listing for stocks, need filters.</td>
      </tr>
      <tr>
        <td>US Code on Main listing</td>
        <td><tag us-code="88160R101" return-main-listing="true" fallback-ticker="TSLA"/></td>
        <td># ask SYM to return the instrument listed on primary exchange</td>
      </tr>
      <tr>
        <td>ISIN</td>
        <td><tag isin="US88160R1014" fallback-ticker="TSLA"/></td>
        <td># likely NOT unique listing for stocks, need filters.</td>
      </tr>
      <tr>
        <td>ISIN on Main listing</td>
        <td><tag isin="US88160R1014" return-main-listing="true" fallback-ticker="TSLA"/></td>
        <td># ask SYM to return the instrument listed on primary exchange</td>
      </tr>
      <tr>
        <td>Local code</td>
        <td><tag local-code="TSLA" fallback-ticker="TSLA"/></td>
        <td># likely NOT unique listing for stocks, need filters.</td>
      </tr>
      <tr>
        <td>Local code with Country</td>
        <td><tag local-code="TSLA" country-code="US" fallback-ticker="TSLA"/></td>
        <td># likely unique listing for US stocks</td>
      </tr>
      <tr>
        <td>Local code with MIC</td>
        <td><tag local-code="TSLA" operational-mic="XNAS" fallback-ticker="TSLA"/></td>
        <td># likely unique listing</td>
      </tr>
      <tr>
        <td>Local code with MIC and instrument class</td>
        <td><tag local-code="TSLA" operational-mic="XNAS" instrument-class="equity" fallback-ticker="TSLA"/></td>
        <td># likely unique listing</td>
      </tr>
      <tr>
        <td>Fallback ticker</td>
        <td><tag fallback-ticker="TSLA"/></td>
        <td>Always include a fall back to ensure the message will be accepted.</td>
      </tr>
    </tbody>
  </table>
</messageML>
<messageML><chime/></messageML>
// Presentation ML content of the message
<div data-format="PresentationML" data-version="2.0" class="wysiwyg">
  <p><span class="entity" data-entity-id="0">$SAN</span></p>
</div>

// Entities (entityJSON field in the message):
{
  "0": {
    "id": [
      {
        "type": "org.symphonyoss.fin.security.id.ticker",
        "value": "SAN"
      }
    ],
    "type": "org.symphonyoss.fin.security",
    "version": "1.0"
  }
}
// Presentation ML content of the message
<div data-format="PresentationML" data-version="2.0" class="wysiwyg">
  <p><span class="entity" data-entity-id="0">$SAN</span></p>
</div>

// Entities (entityJSON field in the message):
{
  "0": {
    "id": [
      {
        "type": "org.symphonyoss.fin.security.id.ticker",
        "value": "SAN"
      },
      {
        "type": "org.symphonyoss.fin.security.id.uniqueId",
        "value": "91e3e37d-da68-42e8-ba05-94aa51aebb4e"
      },
      {
        "type": "org.symphonyoss.fin.security.id.fullBbgTicker",
        "value": "SAN FP Equity"
      },
      {
        "type": "org.symphonyoss.fin.security.id.isin",
        "value": "FR0000120578"
      },
      {
        "type": "org.symphonyoss.fin.security.id.figi",
        "value": "BBG000BWBBP2"
      },
      {
        "type": "org.symphonyoss.fin.security.id.localCode",
        "value": "SAN"
      },
      {
        "type": "org.symphonyoss.fin.security.id.operationalMic",
        "value": "XPAR"
      },
      {
        "type": "org.symphonyoss.fin.security.exchangeName",
        "value": "Euronext Paris"
      },
      {
        "type": "org.symphonyoss.fin.security.country",
        "value": "France"
      },
      {
        "type": "org.symphonyoss.fin.security.displayName",
        "value": "Sanofi"
      },
      {
        "type": "org.symphonyoss.fin.security.currency",
        "value": "EUR"
      },
      {
        "type": "org.symphonyoss.fin.security.instrumentTypeCode",
        "value": "EQS"
      },
      {
        "type": "org.symphonyoss.fin.security.instrumentTypeName",
        "value": "Equity Shares"
      },
      {
        "type": "org.symphonyoss.fin.security.rootBbgCompTicker",
        "value": "SAN"
      },
      {
        "type": "org.symphonyoss.fin.security.bbgcompticker",
        "value": "SAN FP"
      }
    ],
    "type": "org.symphonyoss.fin.security",
    "version": "2.0"
  }
} 
color options
color options
color options
shorthand tags
color options
Structured objects
color options
Special Characters
Create Message v4
Blast Message
Update Message
here
Suppress Message
Apache Freemarker
directly on Symphony Messaging
Agent API
Symphony Messaging Datafeed
At-least-once Delivery mechanism with Amazon AWS SQS
Create Message
Create Message
Symphony Messaging Elements
UI Style Guide
Message Sent
Messages Suppressed
Symphony Elements Action
Shared Wall Post
IM Created
Room Created
Room Updated Message
Room Deactivated Message
Room Reactivated Message
User Requested to Join Room
User Joined Room
User Left Room
Room Member Promoted To Owner
Room Member Demoted From Owner
Connection Requested
Connection Accepted
Generic System Event
here

Tags

Description

Attributes

<img src="url"/>

• src • class

Tag

Description

Optional attributes

<h1>, <h2>, <h3>, <h4>, <h5>, <h6>

Heading text. 6 levels.

• class

<p>paragraph</p>

Paragraph formatting.

• class

<hr />

Horizontal rule.

None.

<ul> <li>list item</li> </ul>

Unordered or bullet list. Cannot be empty, must contain at least one child <li> item.

• class

<ol> <li>list item</li> </ol>

Numbered list. Cannot be empty, must contain at least one child <li> item.

• class

<div>paragraph</div>

<card> (see example below)

Inserts a card. It contains two different sections: • the <header> (always visible) • the <body> (hidden)

<expandable-card> (see example below)

Inserts a card with new styles and multiple levels of display within the card

• state (mandatory) in <expandable-card> can take 3 values: - "collapsed": only header is visible - "cropped": card expanded but the body is cropped - "expanded": card fully expanded • variant (optional) in <body>: defines the style of the card. It can be either "default" for the default blue style, or "error" for the red error style

Icon set for Buttons

Please find below the list of icons that can be used with Elements Buttons.

Buttons

Buttons are the Symphony elements responsible for submitting a form to the bot. As a result, all Symphony form elements are required to have at least one button where `type=action`. When an end-user clicks this button, the form and the value of each of the elements inside will be submitted to the bot via the datafeed and presented as a JSON payload.

In addition, some forms can contain reset buttons. These buttons are used to reset a form back to its original state.

Buttons support six different styles: primary, primary-link, secondary, tertiary, destructive and destructive-link. Each of those has different colors to suit different actions (to convey meaning). Use the class attribute to specify the style.

  • Primary: use the Primary button when there is a clear primary action on a message. You can use it for the submit button, for example.

  • Secondary: use the Secondary button when there are multiple actions of the same importance or some actions with less importance than a single primary action.

  • Destructive: use the Destructive button when an action results in the removal of an item or if it can result in a potentially serious negative consequence.

  • Primary-link, Tertiary and Destructive-link: These styles are variations respectively of the Primary, Secondary and Destructive buttons but without borders. They are low prominence options that can be used alongside a Primary or as standalone buttons with the ability to read more information.

Attributes

Attribute

Type

Required?

Description

name

String

Yes

Identifies the clicked button.

type

String

No If type is not specified, the default value will be type=”action”

Indicates whether the button is an action button or a reset button. When clicked, the action button sends the form information to the datafeed. On the other hand, the reset button resets the form-data to its initial values. Accepted values: action and reset.

class

String

No

Toggle between new palette of colors: primary, secondary, destructive, primary-link, tertiary and destructive-link.

icon

String

No

Rules and Limitations

  • If class is not defined, the action button assumes the primary class by default. Action buttons should be used for affirmation or confirmation actions.

  • Reset buttons have the secondary class set by default. Reset buttons should be used when the content of the fields need to return to their original state.

Examples

<messageML>
  <form id="form_id">
    <text-field name="text-field" placeholder="Add your comment here" required="true">Initial value</text-field>
    <button type="reset">Reset</button>
    <button name="submit_button" type="action">Submit</button>    
  </form>
</messageML>
<button name="send-primary" type="action" class="primary">Primary Button</button>
<button name="send-secondary" type="action" class="secondary">Secondary Button</button>
<button name="send-tertiary" type="action" class="tertiary">Tertiary Button</button>
<button name="send-destructive" type="action" class="destructive">Destructive Button</button>
{
        "id": "UW2p27",
        "messageId": "4KrVjUU4gnGziWnlqMmD2n___oxo916XbQ",
        "timestamp": 1595966792040,
        "type": "SYMPHONYELEMENTSACTION",
        "initiator": {
            "user": {
                "userId": 7078106482890,
                "firstName": "User",
                "lastName": "Bot",
                "displayName": "User",
                "email": "user_bot@symphony.com",
                "username": "user_bot"
            }
        },
        "payload": {
            "symphonyElementsAction": {
                "stream": {
                    "streamId": "iMft6PLA4lHrEA9icKJobX___oyCKdVVdA",
                    "streamType": "ROOM"
                },
                "formMessageId": "zGeog3OqoYqVI2lwcX2o1X___oxo-A_ubQ",
                "formId": "form_id",
                "formValues": {
                    "action": "submit_button",
                    "init": "hello, my name is John Smith"
                }
            }
        }
    },

Versions and Compatibility

Main features introduced
Agent needed to parse message sent by the bot
Client 2.0 release
Mobile

Initial release

2.55.9

Since first version

Since first version

Reset features behaviour is to show back the initial value of the form

2.55.9

Since first version

Since first version

New styles: • New designs for the buttons • Styles primary destructive and secondary destructive are deprecated • Styles tertiary and destructive are introduced

20.6

Since first version

Since first version

New styles: primary-link and destructive-link Support for icons.

23.11

23.12

Supported

Elements Interactive Forms

Symphony Messaging Elements Forms enable bots to send messages that contain interactive forms with pre-designed text fields, dropdown menus, person selectors, buttons and more. This allows bots to interact with users in a very easy and guided way.

Here after, you will find a brief introduction of how to send Elements, then an update of the message flow for Elements, and finally the form specifications.

Sending Elements

To start using Symphony Messaging Elements, you first need to create a form element using the <form> MessageML tag. The form element can be considered the "frame" of a message, containing elements that will be sent by the bot and subsequently read by the datafeed.

You can see below an example of how a user can interact with a form that was sent by a bot, as well as the messageML structure of the message that was sent by the bot, and the payload that is generated and therefore delivered to the bot via the datafeed, containing the information completed and submitted by the user. Please use the tabs to navigate between these 3 documents.

Known Limitations

  • Once the user has submitted the form, it becomes disabled. However, if the conversation is reloaded, the form resets and the user is able to send a new reply. If your workflow requires a single reply per user, please implement this control on the Bot side.

To begin leveraging Symphony Messaging Elements in your bot workflows continue onto our Available Elements that you can find in the subpages.

Message Flow (for Forms)

Every message exists as part of a flow, in a continuum of events that results in user interaction.

Here is that flow in colorful diagram form, for you to know more about each stage of the message:

  1. A Bot sends a message with Symphony Messaging Elements in a form

  2. The message/from is visible to users. Users interact with the elements

  3. Once submitted, the data is submitted to the bot

  4. Bots can access the data, by reading the datafeed.

Form specification

MessageML Tag

Forms are represented by the <form> tag, as you can see in the examples above.

Attributes

Attribute

Type

Required?

Description

id

String

Yes

Identifies the form.

multi-submit

String

No

Specifies that the form can be submitted several times by the user and the reset behavior. See the possible values and behaviors in Rules and Limitations below.

Rules and Limitations

  • The form element can be considered the "frame" of a message, containing elements that will be sent by the bot and subsequently read by the datafeed.

  • All of the data within a form element will be sent to a bot via the datafeed when a user clicks one of the action buttons in that form. The name attribute of the button will be the value of the action field within the datafeed payload. That way the bot manager can know which button triggered the submission of that form. Starting with Agent 23.11, the auto-submit feature allows to submit the form with a TextField, or with a DropDown Menu.

  • If there is more than one element in the form having the same name attribute, the value is converted to an array. Every index of the array is related to a specific element value. The index order is not guaranteed, so the developer needs to iterate through this array and collect the values.

  • When a form is submitted and multi-submit attribute is not specified, all the elements within it will be disabled, not being possible to edit or resend the same form. However, if the page is refreshed, the user can fill out the form again and submit it as a new form.

  • The attribute multi-submit allows the us to submit the form several times even without needing to refresh the page. It can take 2 different string values:

    • reset which resets the form to the default value when enabling it again for the user,

    • no-reset which keeps the values that were submitted by the user when enabling it again for the user.

Examples

The following example shows three forms being used as follows:

  • The first form (default) shows how Symphony Messaging users interact with a form, and how the Elements are disabled once the form is submitted.

  • The second form (multi-submit-reset) shows how to create a form that can be submitted several times and which resets all its Elements to their default value once the user submits it. You note the Elements are first disabled while loading, then the button shows the user the form has been submitted, before the form is enabled back to its default value for the user to submit it again.

  • The third form (multi-submit-no-reset) shows how to create a form that can be submitted several times and which keeps last submitted values once the user submits it. You note the Elements are first disabled while loading, then the button shows the user the form has been submitted, before the form is enabled back to its last submitted value for the user to submit it again.

<messageML>
  <form id="default">
    <text-field name="test"> This form becomes disabled once submitted</text-field>
    <button name="default">Submit</button>
  </form>
  <hr/>
  <form id="multi-submit-reset" multi-submit="reset">
    <text-field name="test" label="Resets values to default"> This form can be submitted more than once</text-field>
    <button name="multi-submit-reset">Submit</button>
  </form>
  <hr/>
  <form id="multi-submit-no-reset" multi-submit="no-reset">
    <text-field name="test" label="Keeps last submitted values"> This form  can be submitted more than once</text-field>
    <button name="multi-submit-no-reset">Submit</button>
  </form>
</messageML>

Versions and Compatibility

Main features introduced

Agent needed to parse message sent by the bot

Client 2.0 release

Client 1.5 release

Backward client-compatibility behavior (e.g. external rooms)

Initial release

2.55.9

Since first version

1.55

Not working

Multi-submit attribute

20.13

21.8

-

-

Masked Text Field

see Text Field for more information

The masked text input is a single-line plain text editor control in which the text is masked by replacing each character with a dot (•) symbol, providing a way for the user to enter data without people over their shoulder being able to see what they are entering.

Use the maxlength and minlength attributes to specify the maximum and minimum length of the value that can be entered.

Attributes

Attribute

Type

Required?

Description

name

String

Yes

Identifies the input content.

placeholder

String

No

Specifies a short hint that describes the expected value of the input field.

required

Boolean

No

If true, it specifies that the input field must be filled out before submitting the form. Accepted values; true and false.

masked

Boolean

Yes

In case you want to include a text field with masked characters [hidden by asterisk (*) symbols] , you must set maskedas true. If true, it creates a masked text field with hide/show options.

maxlength

Integer

No

The maxlength attribute allows you to specify a maximum number of characters that the user can input.

minlength

Integer

No

The minlength attribute allows you to specify a minimum number of characters that the user can input.

pattern

String

No

Regex String to match for input validation

pattern-error-message

String

No

Error message returned to user if pattern parameter matches user input

title

It accepts a simple text and \n for line breaks

No

The description that will be displayed when clicking the tooltip icon located on top of the Masked Text Field Element. Max length: 256 characters. Available from Symphony v20.8 and above.

label

String

Not required but it is recommended if title is defined

Definition of the label that will be displayed on top of the Masked Text Field Element. Available from Symphony v20.8 and above.

Using Input Validation

Rules and Limitations

  • The masked text field has a max number of 128 characters.

  • The masked text field must be a self-closing tag or have no children.

  • The masked text field is a feature that only hides the content in the UI. Even though it is masked in the UI, the text will be submitted in clear and processed & encrypted the same way as any other text entered in Symphony.

  • Compliance Officers have access to the content of the masked text field.

Examples

The following examples show masked text-fields being used as follows:

  • The first masked text-field (init) shows how to display a default text ("With initial value"). Note that the default text would have been sent to the payload if it had not been deleted before submitting the form.

  • The second masked text-field (placeholder) shows how a placeholder text ("Only Placeholder") is displayed in the UI. Please note the placeholder text is not masked nor sent in the payload if no text has been entered in the field by the enduser.

  • The third masked text-field (noreq) shows how a user can interact with a non-required field. Even if the field is empty (only a placeholder text is present but does not count as a value), it does not prevent the enduser from submitting the form.

  • The fourth masked text-field (req) shows the behaviour of the unique required field of the form, which cannot be submitted in case it is not filled; an error is displayed under the field in case the user submits the form with this empty field.

  • The fifth masked text-field (regex) shows the behaviour of the field when a regex pattern is entered. You can note that the pattern-error-message is displayed under the field if the input does not follow the pattern required by the bot developer.

  • The sixth masked text-field (min) shows how to force users to enter an input with a minimum number of characters, and how an error message is displayed under the field if the input does not respect the minlength required.

  • The seventh masked text-field (label) shows how a label text ("My Label") is displayed.

  • The eighth masked text-field (tooltip) shows how a title text ("My Tooltip/n With a second line") is inserted in the UI under the (i) icon, and how the text entered in the title parameter is displayed when the enduser clicks on the icon.

<messageML>
  <form id="form_id">
    <h2>masked text-fields</h2>
      <text-field masked="true" name="init">With initial value</text-field>
      <text-field masked="true" name="placeholder" placeholder="Only Placeholder"></text-field>
      <text-field masked="true" name="noreq" placeholder="Not required"></text-field>
      <text-field masked="true" name="req" required="true" placeholder="Required"></text-field>
      <text-field masked="true" name="regex" pattern="^[a-zA-Z]{3,3}$" pattern-error-message="My error message - must contain exactly 3 letters">Regex</text-field>
      <text-field masked="true" name="min" placeholder="With min length" minlength="3"></text-field>
      <text-field masked="true" name="label" label="My Label">With Label</text-field>
      <text-field masked="true" name="tooltip" title="My Tooltip\n With a second line">With Tooltip</text-field>
      <button name="maskedtext-field">Submit</button>
  </form>
</messageML>
[
    {
        "id": "q6eUgG",
        "messageId": "NowSKCnJJBdPOXQyoPQg8X___pQDVWaBbQ",
        "timestamp": 1563312167294,
        "type": "SYMPHONYELEMENTSACTION",
        "initiator": {
            "user": {
                "userId": 7078106482890,
                "firstName": "User",
                "lastName": "Bot",
                "displayName": "User",
                "email": "user_bot@symphony.com",
                "username": "user_bot"
            }
        },
        "payload": {
            "symphonyElementsAction": {
                "actionStream": {
                    "streamId": "0YeiA-neZa1PrdHy1L82jX___pQjntU-dA"
                },
                "formStream": {
                    "streamId": "YuK1c2y2yuie6+UfQnjSPX///pQEn69idA=="
                },
                "formMessageId": "5iSJ+faXx/23Jkehx3lpSn///pQDVedXdA==5587",
                "formId": "form_id",
                "formValues": {
                    "action": "maskedtext-field",
                    "init": "",
                    "placeholder": "",
                    "noreq": "",
                    "req": "test",
                    "regex": "Reg",
                    "min6": "abc",
                    "label": "With Label",
                    "tooltip": "With Tooltip"
                }
            }
        }
    }
]

Text Field

Attributes

Attribute

Type

Required?

Description

name

String

Yes

Identifies the text field.

placeholder

String

Optional

Specifies a short hint that describes the expected value of the input field.

required

Boolean

Optional

If true, it specifies that the input field must be filled out before submitting the form.

masked

Boolean

Optional

maxlength

Integer

Optional

The maxlength attribute allows you to specify a maximum number of characters that the user can input.

minlength

Integer

Optional

The minlength attribute allows you to specify a minimum number of characters that the user can input.

pattern

String

Optional

pattern-error-message

String

Optional but if pattern is defined, the pattern-error-message attribute is mandatory.

Error message returned to user if pattern parameter matches user input

title

It accepts a simple text and \n for line breaks

Optional

The description that will be displayed when clicking the tooltip icon located on top of the Text Field Element. Max length: 256 characters. Available from Symphony v20.8 and above.

label

String

Not required but it is recommended if title is defined

Definition of the label that will be displayed on top of the Text Field Element. Available from Symphony v20.8 and above.

auto-submit

Boolean

Optional.

Default false.

When enabled, typing <enter> key in the field will submit the form.

Using Input Validation

Rules and Limitations

  • The text field cannot have children tags but it can have a default text (initial value) between the <text-field></text-field> tags. See Examples below for more details.

  • Text fields are grouped at a max of 4 per row, depending on the screen size. For more information, refer to Alignment of input texts.

  • You can add a default text in your text field by including it between the <text-field></text-field> tags. Note that unlike the placeholder text, the default text will be sent with the form if not edited by the user.

  • Input Validation - Pattern: the max length for all attributes is set to 256.

Examples

The following examples show text fields being used as follows:

  • The first text-field (init) shows how to display a default text ("With initial value"). Note that the default text would have been sent to the payload if it had not been deleted before submitting the form.

  • The second text-field (placeholder) shows how a placeholder text ("Only Placeholder") is displayed in the UI. Please note the placeholder text is not sent in the payload if no text has been entered in the field by the enduser.

  • The third text-field (noreq) shows how a user can interact with a non-required field. Even if the field is empty (only a placeholder text is present but does not count as a value), it does not prevent the enduser from submitting the form.

  • The fourth text-field (req) shows the behaviour of the unique required field of the form, which cannot be submitted in case it is not filled; an error is displayed under the field in case the user submits the form with this empty field.

  • The fifth text-field (regex) shows the behaviour of the field when a regex pattern is entered. You can note that the pattern-error-message is displayed under the field if the input does not follow the pattern required by the bot developer.

  • The sixth text-field (min) shows how to force users to enter an input with a minimum number of characters, and how an error message is displayed under the field if the input does not respect the minlength required.

  • The seventh text-field (label) shows how a label text ("My Label") is displayed.

  • The eighth text-field (tooltip) shows how a title text ("My Tooltip/n With a second line") is inserted in the UI under the (i) icon, and how the text entered in the title parameter is displayed when the enduser clicks on the icon.

<messageML>
  <form id="form_id">
    <h2>text-fields</h2>
      <text-field name="init">With initial value</text-field>
      <text-field name="placeholder" placeholder="Only Placeholder"></text-field>
      <text-field name="noreq" placeholder="Not required"></text-field>
      <text-field name="req" required="true" placeholder="Required"></text-field>
      <text-field name="regex" pattern="^[a-zA-Z]{3,3}$" pattern-error-message="My error message - must contain exactly 3 letters">Regex</text-field>
      <text-field name="min" placeholder="With min length" minlength="3"></text-field>
      <text-field name="label" label="My Label">With Label</text-field>
      <text-field name="tooltip" title="My Tooltip\n With a second line">With Tooltip</text-field>
      <button name="text-field">Submit</button>
  </form>
</messageML>
[
    {
        "id": "q6eUgG",
        "messageId": "NowSKCnJJBdPOXQyoPQg8X___pQDVWaBbQ",
        "timestamp": 1563312167294,
        "type": "SYMPHONYELEMENTSACTION",
        "initiator": {
            "user": {
                "userId": 7078106482890,
                "firstName": "User",
                "lastName": "Bot",
                "displayName": "User",
                "email": "user_bot@symphony.com",
                "username": "user_bot"
            }
        },
        "payload": {
            "symphonyElementsAction": {
                "actionStream": {
                    "streamId": "0YeiA-neZa1PrdHy1L82jX___pQjntU-dA"
                },
                "formStream": {
                    "streamId": "YuK1c2y2yuie6+UfQnjSPX///pQEn69idA=="
                },
                "formMessageId": "5iSJ+faXx/23Jkehx3lpSn///pQDVedXdA==5587",
                "formId": "form_id",
                "formValues": {
                    "action": "text-field",
                    "init": "",
                    "placeholder": "",
                    "noreq": "",
                    "req": "test",
                    "regex": "Reg",
                    "min6": "abc",
                    "label": "With Label",
                    "tooltip": "With Tooltip"
                }
            }
        }
    }
]

Alignment of text-fields

Please note that several text-fields are aligned next to another. Note also that this behaviour is reactive to the screen size, the number of text-fields on the same line decreasing until one per row, as the screen gets smaller (see in the example below).

Versions and Compatibility

Main features introduced
Agent needed to parse message sent by the bot
Client 2.0 release
Mobile

Initial release

2.55.9

Since first version

Since first version

Regex

20.6

Since first version

Since first version

Label

20.7

Since first version

Since first version

Tooltip (title)

20.7

Since first version

Since first version

auto-submit

23.11

23.12

Not supported yet.

Dropdown Menu

A dropdown menu is a static list of items that appears whenever a piece of text or a button is clicked. This is a graphical approach presented to users from which they can choose one or several values from the list presented.

A dropdown menu is also known as a pull-down menu, pull-down list, dropdown list or dropdown box.

The dropdown menu is represented by the <select> tag which provides a menu of <options>. Each <option> element should have a value attribute containing the data value to submit to the server when that option is selected; You can also include a selected attribute on an <option> element to make it selected by default when the page first loads.

Attributes

Rules and Limitations

The <select> tag:

  • The <select> tag stands for our dropdown parent tag, which has <options> as its children; one for each select.

  • Select tags only accept <option> tags as children. The <select> tag must contain at least one <option> tag.

  • The only valid attributes of the <select> tag are name and required.

  • Note that, by default, Symphony users will only be able to select one option from the dropdown menu. However, using the attribute multiple set to true together with min and max attributes, users will be able to select several options from the dropdown. Please see below the examples to know how to use these attributes.

The <option> tag:

  • The <option> tag cannot have other <option> tags as children. The only valid child of a <option> tag is a text node, which specifies the text that will be displayed for that option inside the dropdown menu. The text node is also required.

  • The only valid attributes of the <option> tag are value and selected.

  • Only one <option> of a given select can have the attribute selected as true.

If neither the selected or data-placeholder attributes are set, the default text (title) of the dropdown menu will be "Dropdown".

Message Size Limit:

Examples

The following examples show dropdown menus being used as follows:

  • The first dropdown (init) shows how to display a default preselected option ("opt2": "With selected option"). Note that the preselected option is sent to the payload when submitting the form.

  • The second dropdown (data-placeholder) shows how a placeholder text ("Only data-placeholder") is displayed in the UI. Please note the placeholder text is not sent in the payload if no option from the dropdown menu has been selected by the enduser.

  • The third dropdown (noreq) shows how a user can interact with a non-required field. Even no option is selected by the user, it does not prevent the enduser from submitting the form.

  • The fourth dropdown (req) shows the behaviour of the unique required field of the form, which cannot be submitted in case no option from the dropdown menu is selected by the user; an error is displayed under the field in case the user submits the form with this empty field.

  • The fifth dropdown (label) shows how a label text ("My Label") is displayed.

  • The sixth dropdown (tooltip) shows how a title text ("My Tooltip/n With a second line") is inserted in the UI under the (i) icon, and how the text entered in the title parameter is displayed when the enduser clicks on the icon.

  • The seventh dropdown (multiple) shows how to combine multiple attribute with min/max rules to guide users selecting between 3 and 5 options.

Versions and Compatibility

Text Area

The textarea element is a field for multi-line text input, allowing users to edit multiple lines of plain text. Text areas are useful to collect or edit runs of text like messages, opinions, reviews, articles, etc.

Attributes

Using Input Validation

Rules and Limitations

  • The text field must be a self-closing tag or have no children.

  • You can add a default text in your text area by including it between the <textarea></textarea> tags. Note that unlike the placeholder text, the default text will be sent with the form if not edited by the user. Refer to Examples for more information.

Examples

The following example shows two textareas being used as follows:

  • The first textarea (id1) shows how to display a default text ("With initial value"). Note that the default text would have been sent to the payload if it had not been deleted before submitting the form.

  • The second text-field (req) shows how a placeholder text ("Required, with a placeholder, a regex, a label, and a tooltip") is displayed in the UI. Please note the placeholder text is not sent in the payload if no text has been entered in the field by the enduser. It shows as well the behaviour of a required textarea in a form, which cannot be submitted in case it is not filled; an error is displayed under the textarea in case the user submits the form with this empty field. The textarea presents how a label text ("My Label") as well as a title text ("My Tooltip/n With a second line") are displayed in the UI. Finally, it shows how users can interact with a regex pattern which does not allow the form to be submitted if the input does not follow the pattern required by the bot developer.

Versions and Compatibility

Radio Button

Radio buttons are shown as small circles, which are filled or highlighted when selected. Only one radio button in a given group can be selected at the same time.

Frequently, a set of radio buttons represents a single question which the user can answer by selecting a possible answer.

Attributes

Rules and Limitations

  • The text node of the MessageML will be converted to the <label> tag. This will preserve the formatting tags <i> and <b>, if present.

  • Radio buttons are presented in radio groups (a collection of radio buttons describing a set of related options). Only one radio button in a group can be selected at the same time. Note: The radio group must share the same name (the value of the name attribute) to be treated as a group. Once the radio group is created, selecting any radio button in that group automatically deselects any other selected radio button in the same group.

  • A form can have a maximum of 50 radio buttons. Note: The limit in previous versions was set to 20, so this limit may still apply when sending messages to customers with an earlier version of Symphony (before 20.10).

  • Once the user has selected a radio option, it can be deselected only by clicking on another radio option. The only way to deselect all the radio options is by clicking the reset button.

Examples

The following example shows radio buttons being used. It shows how developers can use the checked parameter with the value01 preselected when the form is sent. It also shows how users can select another radio button and how it automatically unselect any other value checked, as the 3 radio buttons have the same name and therefore are part of the same group "groupId".

Versions and Compatibility

Checkbox

The checkbox is an interactive box that can be toggled by the user to indicate an affirmative or negative choice.

When clicked, a checkmark (✓) appears inside the box, to indicate an affirmative choice (yes). When clicked again, the checkmark disappears, indicating a negative choice (no).

Checkboxes are used to let a user select one or more options from a limited number of choices. Frequently, a set of checkboxes represents a single question which the user can answer by selecting any number of possible answers.

Attributes

Rules and Limitations

  • The text node of the MessageML will be converted to the <label> tag. This will preserve the formatting tags <i> and <b>, if present.

  • A form can have a maximum of 50 checkboxes within it. Note: The limit in previous versions was set to 20, so this limit may still apply when sending messages to customers with an earlier version of Symphony (before 20.10).

  • Once selected, checkboxes can be deselected by clicking them.

  • Click the reset button to return the checkboxes to their original status (checked or unchecked).

  • If a checkbox is sent without at least one checked option, it will not be displayed in the datafeed payload.

Examples

The following example shows checkboxes being used. It shows how developers can use the checked parameter with the value01 preselected when the form is sent. It also shows how users can select or unselect one or several checkboxes before submitting the form, as well as how to reset it to its initial values.

Versions and Compatibility

Person Selector

The Person Selector is an element used for finding and selecting people. Person Selectors are used in many places within Symphony, so you should be familiar with how they work.

When a user types the person's name, a drop-down will be displayed with the results found for the data the user entered. The following example shows how three people with the same name (Vincent) have been found.

Attributes

Rules and Limitations

  • The Person Selector element supports multi-user selection which means that you can search for more than one person using the same selector.

Examples

The following examples show person selectors being used as follows:

  • The first person-selector (placeholder) shows how a placeholder text ("My Placeholder") is displayed in the UI. Please note the placeholder text is not sent in the payload if no option from the dropdown menu has been selected by the enduser.

  • The second person-selector (placeholder) shows how the bot can introduce a default value in the person selector. It shows as well how a Symphony user can interact with the clear all functionality.

  • The third person-selector (noreq) shows how a user can interact with a non-required field. Even if nobody is selected by the user, it does not prevent the enduser from submitting the form.

  • The fourth person-selector (req) shows the behaviour of the unique required field of the form, which cannot be submitted in case nobody from the person selector is selected by the user; an error is displayed under the field in case the user submits the form with this empty field. An auto-filtering behaviour allows the user to see less options as he digits some input. Also, it shows how Symphony users can remove some of the selected users with the cross associated to that specific user.

  • The fifth person-selector (label) shows how a label text ("My Label") is displayed.

  • The sixth person-selector (tooltip) shows how a title text ("My Tooltip/n With a second line") is inserted in the UI under the (i) icon, and how the text entered in the title parameter is displayed when the enduser clicks on the icon.

User ID

The result returned by the datafeed for the selected users is an array of user Ids, which is an array of long.

Versions and Compatibility

<messageML>
    These are special characters:
    <ul>
        <li>&lt;;</li>
        <li>&#38;;</li>
        <li>$ or &#36;;</li>
        <li># or &#35;;</li>
        <li>> or &gt;;</li>
        <li>" or &quot;;</li>
        <li>' or &#39;;</li>
        <li>* or &#42;;</li>
        <li>% or &#37;.</li>
    </ul>
</messageML>
<messageML>
    <h1>Heading texts</h1>
        <h1>This is a h1 heading text</h1>
        <h2>This is a h2 heading text</h2>
        <h3>This is a h3 heading text</h3>
        <h4>This is a h4 heading text</h4>
        <h5>This is a h5 heading text</h5>
        <h6>This is a h6 heading text</h6><br/>
    <h1>Paragraphs</h1>
        <p>This is a paragraph.</p>
        <div style="color:red;">
            <p>This is a paragraph enclosed in a div that allows its text to be red.</p>
        </div><br/>
    <h1>Horizontal Rule</h1>
        Below you can see an horizontal rule<hr /><br/>
    <h1>Lists</h1>
        This is an unordered/bullet list:
        <ul>
            <li>Item 1</li>
            <li>Item 2</li>
        </ul>
        This is a numbered list:
        <ol>
            <li>Item 1</li>
            <li>Item 2</li>
        </ol><br/>
    <h1>Cards</h1>
        <card accent="tempo-bg-color--blue">
            <header>This is the header of a card</header>
            <body>This is the body of a card</body>
        </card>
        <expandable-card state="expanded">
            <header>This is the header of an expanded expandabled-card</header>
            <body variant="error">This is an error variant of the body of an expanded expandable-card</body>
        </expandable-card> 
</messageML>
Instrument lookup type-ahead by ticker, name, ISIN, etc.
New hovercard for the enhanced tags

Image. Images have a max height of 256px; otherwise, the default size is the size of the image. For more information on how to send images through API call, refer to .

Block of text. • This tag can be used to specify visual styles, by adding a class attribute. • This tag is used to create . • This tag is also the root of any message read through the API.

• class: • data-entity-id • data-icon-src • data-accent-color • See below for list of translated PresentationML attributes.

• iconSrc: image will be resized to 28 pixels by 28 pixels, use spacious mode. (.jpg, .png and .gif) • accent: use to select the accent color of the card.

Adds an icon before the button name. Only icons from our are supported. Each icon is identified by its name.

The following example shows the use of the Reset and the Submit button when sending a text inserted in a .

By reusing our pre-designed standard UX component libraries, Elements provide developers with out-of-the-box tools to easily create interactive bot messages that look and feel like they belong in Symphony Messaging. To use the Elements, you just need to call the endpoint with your bot, using the MessageML format.

<messageML> 
    <form id="form_id"> 
        <h4>Personal Information</h4>
        <text-field name="name" required="true" placeholder="Name" />
        <text-field name="email" required="true" placeholder="email" />

        <h4>Select your country</h4>
        <select name="country">
            <option value="opt1">Australia</option>
            <option value="opt2">Brazil</option>
            <option value="opt3">China</option>
            <option value="opt4">Denmark</option>
            <option value="opt5">Ecuador</option>
            <option value="opt6">France</option>
            <option value="opt7">Germany</option>
            <option value="opt8">Italy</option>
            <option value="opt9">Japan</option>
        </select>

        <h4>Choose your option</h4>            
        <radio name="example_radio" value="option_01" checked="true">Marked</radio>
        <radio name="example_radio" value="option_02">Unmarked</radio>

        <h4>Choose your option</h4> 
        <checkbox name="checkbox_1" value="value01" checked="true">Checked</checkbox>
        <checkbox name="checkbox_2" value="value02">Unchecked</checkbox>

        <h4>Send a comment</h4> 
        <textarea name="comment" placeholder="Add your comment here" required="true" />

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

    </form>
</messageML>
[
    {
        "id": "Y6OwLV",
        "messageId": "N0DEie_ig1_1MwfGQq0df3___oNnFeTobQ",
        "timestamp": 1634653051671,
        "type": "SYMPHONYELEMENTSACTION",
        "initiator": {
            "user": {
                "userId": 11338713661667,
                "firstName": "Thibault",
                "lastName": "Chays",
                "displayName": "Internal User",
                "email": "thibault.chays@symphony.com",
                "username": "thibault.chays"
            }
        },
        "payload": {
            "symphonyElementsAction": {
                "stream": {
                    "streamId": "usnBKBkH_BVrGOiVpaupEH___okFfE7QdA",
                    "streamType": "IM"
                },
                "formMessageId": "L87DKjjrGhjH3_WN8_rqYn___oNnJZ9AbQ",
                "formId": "form_id",
                "formValues": {
                    "action": "submit_button",
                    "name": "John",
                    "email": "john@email.com",
                    "country": "opt1",
                    "example_radio": "option_01",
                    "checkbox_1": "value01",
                    "checkbox_2": "value02",
                    "comment": "test"
                }
            }
        }
    }
]

To be considered valid, the form tag must contain at least one action type "Button" as a child. For more information, refer to .

When designing forms, it is important to consider the message size limit. For more information refer to

With Symphony v20.6, bot developers can use Regex to validate text fields and text areas using the pattern and pattern-error-message attributes. For more information and examples, refer to .

Text input fields are the most common elements in a form. Symphony provides two types of elements for text input fields: are for a single-line input; and is for multi-line input.

If true, it creates a masked text field with hide/show options when its value is "true". For more information, refer to .

Regex String to match for input validation. For more information, refer to .

With Symphony v20.6, bot developers can use Regex to validate text fields and text areas using the pattern and pattern-error-message attributes. For more information and examples, refer to .

The text field has a max number of 128 characters. For larger texts, use .

When designing forms with dropdown menus that include a very large number of options you may hit the characters limit of a Symphony message. For more information about message size limits, refer to .

Main features introduced
Agent needed to parse message sent by the bot
Client 2.0 release
Mobile

With Symphony v20.6, bot developers can use Regex to validate text fields and text areas using the pattern and pattern-error-message attributes. For more information and examples, refer to Regular .

Note: If you want the user to be able to select more than one option, use the element.

Note: If you want the user to only be able to pick a single option, use the or the element.

Sending images
Structured objects
color options
background color values
Text Field
Create Message
Buttons
Regular Expressions - Regex
Text fields
Text area
Regular Expressions - Regex
Text Area
Message size limits.

Attribute

Type

Required?

Description

name

String

Yes

Required attribute of the <select> tag. It identifies the dropdown menu.

required

Boolean

No

Optional attribute of the <select> tag. it is a Boolean attribute indicating that an option with a non-empty string value must be selected.

value

String

Yes

Required attribute of the <option> tag. It contains the data value to submit to the server when that option is selected.

selected

Boolean

Optional

You can include a selected attribute on an <option> element to make it selected by default when the page first loads. Accepted values: true and false.

data-placeholder

String

Optional

Text displayed in the dropdown menu before an option is selected. It can be a short hint or a title for the dropdown menu.

title

It accepts a simple text and \n for line breaks

Optional

The description that will be displayed when clicking the tooltip icon located on top of the Masked Text Field Element. Max length: 256 characters.

label

String

Not required but it is recommended if title is defined

Definition of the label that will be displayed on top of the Masked Text Field Element.

multiple

String Boolean

Optional Default is false

Allows users to select multiple values and the developer to send dropdown with multiple preselected options.

min

String Integer ≥ 2 and ≤ max attribute

Optional Can be used only if multiple attribute is set to true

Minimum number of options to be selected by the Symphony user. NB: If undefined, no minimum option needs to be selected. Please use required attribute if you want to set min to 1.

max

String Integer ≥ 2

Optional Can be used only if multiple attribute is set to true

Maximum number of options to be selected by the Symphony user. NB: If undefined, user will be able to select as many options as wished. Please use multiple attribute set to false if you want to set max to 1.

auto-submit

Boolean

Optional Default is false

When enabled, selecting a value will submit the form.

<messageML>
  <form id="form_id">
    <h2>dropdown menus</h2>
      <select name="init"><option value="opt1">Unselected option 1</option><option value="opt2" selected="true">With selected option</option><option value="opt3">Unselected option 2</option></select>
      <select name="data-placeholder" data-placeholder="Only data-placeholder"><option value="opt1">Unselected option 1</option><option value="opt2">Unselected option 2</option><option value="opt3">Unselected option 3</option></select>
      <select name="noreq" data-placeholder="Not required"><option value="opt1">First</option><option value="opt2">Second</option><option value="opt3">Third</option></select>
      <select name="req" required="true" data-placeholder="Required"><option value="opt1">First</option><option value="opt2">Second</option><option value="opt3">Third</option></select>
      <select name="label" label="My Label" data-placeholder="With Label"><option value="opt1">Unselected option 1</option><option value="opt2">Unselected option 2</option><option value="opt3">Unselected option 3</option></select>
      <select name="tooltip" title="My Tooltip\n With a second line" data-placeholder="With Tooltip"><option value="opt1">Unselected option 1</option><option value="opt2">Unselected option 2</option><option value="opt3">Unselected option 3</option></select>
      <select name="multiple" label="With multiple select options - between 3 and 5" multiple="true" min="3" max="5"><option value="opt1" selected="true">Preselected option 1</option><option value="opt2" selected="true">Preselected option 2</option><option value="opt3" selected="true">Preselected option 3</option><option value="opt4">Unselected option 4</option><option value="opt5">Unselected option 5</option><option value="opt6">Unselected option 6</option></select>
      <button name="dropdown">Submit</button>
  </form>
</messageML>
[    
    {
        "id": "JQgymy",
        "messageId": "h53CRuPWoInmYbfw2T8dkn___pNK27l7bQ",
        "timestamp": 1566407149188,
        "type": "SYMPHONYELEMENTSACTION",
        "initiator": {
            "user": {
                "userId": 7078106482890,
                "firstName": "User",
                "lastName": "Bot",
                "displayName": "User",
                "email": "user_bot@symphony.com",
                "username": "user_bot"
            }
        },
        "payload": {
            "symphonyElementsAction": {
                "actionStream": {
                    "streamId": "0YeiA-neZa1PrdHy1L82jX___pQjntU-dA"
                },
                "formStream": {
                    "streamId": "YuK1c2y2yuie6+UfQnjSPX///pQEn69idA=="
                },
                "formMessageId": "RfqsxcfTHCV08+UcO03HQH///pIqaO6fdA==",
                "formId": "form_id",
                "formValues": {
                    "action": "dropdown",
                    "init": "opt2",
                    "data-placeholder": "",
                    "noreq": "",
                    "req": "opt2",
                    "label": "",
                    "tooltip": "",
                    "multiple": ["opt1", "opt2", "opt3", "opt5", "opt6"]
                }
            }
        }
    }
]

Initial release

2.55.9

Since first version

Since first version

Label

20.6

Since first version

Since first version

Tooltip (title)

20.7

Since first version

Since first version

Multiple (with min and max) attributes

20.14

21.12

Since first version

auto-submit

23.11

23.12

Not supported yet.

Attribute

Type

Required?

Description

name

String

Yes

Identifies the text area.

placeholder

String

No

Specifies a short hint that describes the expected value of the text area.

required

Boolean

No

If true, it specifies that the text area must be filled out before submitting the form. Accepted values; true and false.

pattern

String

No

Regex String to match for input validation

pattern-error-message

String

No

Error message returned to user if pattern parameter matches user input

title

It accepts a simple text and \n for line breaks

No

The description that will be displayed when clicking the tooltip icon located on top of the Masked Text Field Element. Max length: 256 characters. Available from Symphony v20.8 and above.

label

String

Not required but it is recommended if title is defined

Definition of the label that will be displayed on top of the Masked Text Field Element. Available from Symphony v20.8 and above.

rows

Number

No

Specify the number of rows (height) of the text area that will be displayed by default.

cols

Number

No

Specify the number of columns (width) of the text area that will be displayed by default.

<messageML>
  <form id="form_id">
    <h2>textareas</h2>
      <textarea name="id1" >With initial value</textarea>
      <textarea name="req" required="true" label="My label" title="My title\nWith second line" pattern="^[a-zA-Z]{3,3}$" pattern-error-message="My error message - must contain exactly 3 letters" placeholder="Required, with a placeholder, a regex, a label, and a tooltip"></textarea>
      <button name="textarea">Submit</button>
  </form>
</messageML>
[
    {
        "id": "wPptaz",
        "messageId": "LI4WgwZSDcstpKjyeKvCH3___pQEQvHfbQ",
        "timestamp": 1563296599584,
        "type": "SYMPHONYELEMENTSACTION",
        "initiator": {
            "user": {
                "userId": 7078106482890,
                "firstName": "User",
                "lastName": "Bot",
                "displayName": "User",
                "email": "userbot@symphony.com",
                "username": "user_bot"
            }
        },
        "payload": {
            "symphonyElementsAction": {
                "actionStream": {
                    "streamId": "0YeiA-neZa1PrdHy1L82jX___pQjntU-dA"
                },
                "formStream": {
                    "streamId": "YuK1c2y2yuie6+UfQnjSPX///pQEn69idA=="
                },
                "formMessageId": "1P6z5kI6OzkxTKEoKOgWZ3///pQERpkYdA==5338",
                "formId": "form_id",
                "formValues": {
                    "action": "textarea",
                    "id1": "",
                    "req": "abc"
                }
            }
        }
    }
]

Main features introduced

Agent needed to parse message sent by the bot

Client 2.0 release

Backward client-compatibility behavior (e.g. external rooms)

Initial release

2.55.9

Since first version

Not working

Regex

20.6

Since first version

Regex validation not enforced but field can be submitted

Label

20.7

Since first version

Label displayed and form can still be submitted

Tooltip (title)

20.7

Since first version

Tooltip not displayed but form can still be submitted

row and col

23.11

24.1

Not supported.

Attribute

Type

Required?

Description

name

String

Yes

Identifies the radio button.

value

String

No

The value is the string that will be sent to the server. If the value is not specified, the string on will be sent by default.

checked

Boolean

No

If true, it specifies that the <radio> element should be pre-selected (checked) when the page loads. Accepted values: true and false.

<messageML>
  <form id="form_id">
    <h2>radio buttons</h2>
      <radio name="groupId" value="value01" checked="true">red</radio>
      <radio name="groupId" value="value02">green</radio>
      <radio name="groupId" value="value03">blue</radio>
      <button type="reset">Reset</button>
      <button name="radio" type="action">Submit</button>
  </form>
</messageML>
[
    {
        "id": "chxhFk",
        "messageId": "BwcQN6Y7RcKxwpWDfcjL2n___pQD2WPebQ",
        "timestamp": 1563303517217,
        "type": "SYMPHONYELEMENTSACTION",
        "initiator": {
            "user": {
                "userId": 7078106482890,
                "firstName": "User",
                "lastName": "Bot",
                "displayName": "User",
                "email": "user_bot@symphony.com",
                "username": "user_bot"
            }
        },

        "payload": {
            "symphonyElementsAction": {
                "stream": {
                  "streamId": "0YeiA-neZa1PrdHy1L82jX___pQjntU-dA",
                  "streamType": "ROOM"
                },
                "formMessageId": "qXF5jpNbJtlulAmjKjn0Pn///pQD2mc/dA==5935",
                "formId": "form_id",
                "formValues": {
                    "action": "radio",
                    "groupId": "value03"
                }
            }
        }
    }
]

Main features introduced

Agent needed to parse message sent by the bot

Client 2.0 release

Backward client-compatibility behavior (e.g. external rooms)

Initial release

2.55.9

Since first version

Not working

Attribute

Type

Required?

Description

name

String

Yes

Identifies the checkbox.

value

String

No

The value is the string that will be sent to the server. If the value is not specified, the string on will be sent by default.

checked

Boolean

No

If true, it specifies that the <checkbox> element should be pre-selected (checked) when the page loads. Accepted values: true and false.

<messageML>
  <form id="form_id">
    <checkbox name="id1" value="value01" checked="true">Red</checkbox>
    <checkbox name="id2" value="value02" checked="false">Green</checkbox>
    <checkbox name="id3" value="value03" checked="false">Blue</checkbox>
    <button type="reset">Reset</button>
    <button name="example-button" type="action">Submit</button>    
  </form>
</messageML>
[
    {
        "id": "CfVa3O",
        "messageId": "TwSQ5mOSHJHaqABxRBYmmn___pQD5znvbQ",
        "timestamp": 1563302610448,
        "type": "SYMPHONYELEMENTSACTION",
        "initiator": {
            "user": {
                "userId": 7078106482890,
                "firstName": "User",
                "lastName": "Bot",
                "displayName": "User",
                "email": "user_bot@symphony.com",
                "username": "user_bot"
            }
        },
        "payload": {
            "symphonyElementsAction": {
                "stream": {
                  "streamId": "0YeiA-neZa1PrdHy1L82jX___pQjntU-dA",
                  "streamType": "ROOM"
                },
                "formMessageId": "en1LFlhkWTwdXyYvyMsna3///pQD6JE4dA==5856",
                "formId": "form_id",
                "formValues": {
                    "action": "checkbox",
                    "id3": "value03"
                }
            }
        }
    }
]

Main features introduced

Agent needed to parse message sent by the bot

Client 2.0 release

Backward client-compatibility behavior (e.g. external rooms)

Initial release

2.55.9

Since first version

Not working

Attribute

Type

Required?

Description

name

String

Yes

Identifies the person selector

placeholder

String

No

Specifies a short hint that describes the expected value of the input field.

required

Boolean

No

If true, it specifies that the person selector must be filled out before submitting the form, which means that at least one person must be "selected" Accepted values; true and false.

title

It accepts a simple text and \n for line breaks

No

The description that will be displayed when clicking the tooltip icon located on top of the Element. Max length: 256 characters.

label

String

Not required but it is recommended if title is defined

Definition of the label that will be displayed on top of the Masked Text Field Element.

value

Array of

long

No

Default value that will be preselected in the person-selector when the user receive the form from the bot.

<messageML>
  <form id="form_id">
    <h2>person-selectors</h2>
      <person-selector name="withplaceholder" placeholder="My Placeholder"/>
      <person-selector name="withdefaultvalues" label="With default values" value='[12987981109743,12987981109741]' />
      <person-selector name="noreq" placeholder="Not required"/>
      <person-selector name="req" required="true" placeholder="Required"/>
      <person-selector name="withlabel" label="My Label" placeholder="With Label"/>
      <person-selector name="withtooltip" title="My Tooltip\n With a second line" placeholder="With Tooltip"/>
      <button name="person-selector">Submit</button>
  </form>
</messageML>
[
    {
        "id": "RJ4MqQ",
        "messageId": "BnjUz2Q26tKI_kQiplv0IX___pP-I0xXbQ",
        "timestamp": 1563399336872,
        "type": "SYMPHONYELEMENTSACTION",
        "initiator": {
            "user": {
                "userId": 7078106482890, 
                "firstName": "User",
                "lastName": "Bot",
                "displayName": "User",
                "email": "user_bot@symphony.com",
                "username": "user_bot"
            }
        },
        "payload": {
            "symphonyElementsAction": {
                "actionStream": {
                    "streamId": "0YeiA-neZa1PrdHy1L82jX___pQjntU-dA"
                },
                "formStream": {
                    "streamId": "YuK1c2y2yuie6+UfQnjSPX///pQEn69idA=="
                },
                "formMessageId": "ojfA0Eei0kSymzDpX72ysX///pP+I7LjdA==5615",
                "formId": "form_id",
                "formValues": {
                    "action": "person-selectors",
                    "withplaceholder": [],
                    "withdefaultvalues": [
                        12987981109743
                    ],
                    "noreq": [],
                    "req": [
                        7078106482890
                    ],
                    "withlabel": [],
                    "withtooltip": []
                }
            }
        }
    }
]

Main features introduced

Agent needed to parse message sent by the bot

Client 2.0 release

Backward client-compatibility behavior (e.g. external rooms)

Initial release

2.55.9

Since first version

Not working

Label

20.7

Since first version

Label displayed and form can still be submitted

Tooltip (title)

20.7

Since first version

Tooltip not displayed but form can still be submitted

Value

20.12

21.7

For client 1.5, it displays the person-selector as if there was no default value

Message Size Limits

Extensibility UI Actions

With Actions, MessageML allows your bots to send messages containing components that have UI extensibility capabilities, such as opening another chatroom or start a conversation with a specific user for your Symphony users.

These functionalities are gathered in the subpages you can find below.

MessageML tag

The UI Actions are represented by the <ui-action> tag, that can be associated with different attributes, such as trigger, action, or other more specific attributes.

Please note that UI-Actions are not rendered on mobile

UI Toolkit list
Masked Text Field
Regular Expressions - Regex
Expressions - Regex
Checkbox
Radio Button
Dropdown Menu

Table Select

The following image shows three different tables. The first table shows the use of checkboxes to select rows, positioned to the right side of the table. The second example also shows checkboxes, but they are positioned to the left side. The last table shows buttons positioned to the right.

Building a Table Select with Apache FreeMarker

In the JSON data, you can configure the type of the Element that will be added to the Table Select and its position:

Attribute

Type

Required?

Description

type

String

Yes

position

String

Yes

This attribute indicates how the buttons and checkboxes must be aligned inside the column. Accepted values: left or right. For more information, refer to the JSON example below.

In this example, the table type is set as button and the position is set as left.

<#macro createSelectHeader>
    <#if data.select.type=='checkbox'>
        <td>
            <input type="checkbox" name="tablesel-header" />
        </td>
        <#elseif data.select.type='button'>
            <td>Select</td>
    </#if>
</#macro>
<#macro createSelectBody index>
    <#if data.select.type=='checkbox'>
        <td>
            <input type="checkbox" name="tablesel-row-${index}" />
        </td>
        <#elseif data.select.type='button'>
            <td>
                <button name="tablesel-row-${index}">SELECT</button>
            </td>
    </#if>
</#macro>
<#macro createSelectFooter>
    <#if data.select.type=='checkbox'>
        <td>
            <input type="checkbox" name="tablesel-footer" />
        </td>
        <#elseif data.select.type='button'>
            <td>
                <button name="tablesel-footer">SELECT</button>
            </td>
    </#if>
</#macro>
<messageML>
    <form id="example">
        <table>
            <#if data.header?has_content>
                <thead>
                    <tr>
                        <#if data.select.position=='left'>
                            <@createSelectHeader/>
                        </#if>
                        <#list data.header as row>
                            <#list row as cell>
                                <td>${cell}</td>
                            </#list>
                        </#list>
                        <#if data.select.position=='right'>
                            <@createSelectHeader/>
                        </#if>
                    </tr>
                </thead>
            </#if>
            <#list data.body>
                <tbody>
                    <#items as row>
                        <tr>
                            <#if data.select.position=='left'>
                                <@createSelectBody index="${row?counter}" />
                            </#if>
                            <#list row as cell>
                                <td>${cell}</td>
                            </#list>
                            <#if data.select.position=='right'>
                                <@createSelectBody index="${row?counter}" />
                            </#if>
                        </tr>
                    </#items>
                </tbody>
            </#list>
            <#if data.footer?has_content>
                <tfoot>
                    <tr>
                        <#if data.select.position=='left'>
                            <@createSelectFooter/>
                        </#if>
                        <#list data.footer as row>
                            <#list row as cell>
                                <td>${cell}</td>
                            </#list>
                        </#list>
                        <#if data.select.position=='right'>
                            <@createSelectFooter/>
                        </#if>
                    </tr>
                </tfoot>
            </#if>
        </table>
    </form>
</messageML>
{
  "select": {
    "position": "left",
    "type": "button"
  },
  "header": [
    ["H1","H2","H3"]
  ],
  "body": [
    ["A1", "B1", "C1"],
    ["A2", "B2", "C2"],
    ["A3", "B3", "C3"]
  ],
  "footer": [
    ["F1","F2","F3"]
  ]
}
<messageML>
    <form id="example">
        <table>
            <thead>
                <tr>
                    <td>Select</td>
                    <td>H1</td>
                    <td>H2</td>
                    <td>H3</td>
                </tr>
            </thead>
            <tbody>
                <tr>
                    <td>
                        <button type="action" name="tablesel-row-1">SELECT</button>
                    </td>
                    <td>A1</td>
                    <td>B1</td>
                    <td>C1</td>
                </tr>
                <tr>
                    <td>
                        <button type="action" name="tablesel-row-2">SELECT</button>
                    </td>
                    <td>A2</td>
                    <td>B2</td>
                    <td>C2</td>
                </tr>
                <tr>
                    <td>
                        <button type="action" name="tablesel-row-3">SELECT</button>
                    </td>
                    <td>A3</td>
                    <td>B3</td>
                    <td>C3</td>
                </tr>
            </tbody>
            <tfoot>
                <tr>
                    <td>
                        <button type="action" name="tablesel-footer">SELECT</button>
                    </td>
                    <td>F1</td>
                    <td>F2</td>
                    <td>F3</td>
                </tr>
            </tfoot>
        </table>
    </form>
</messageML>
{
    "id": "3dtVXF",
    "messageId": "amKuCXE9wjfEFX7qQPzanX___oyR5rbWbQ",
    "timestamp": 1595280017705,
    "type": "SYMPHONYELEMENTSACTION",
    "initiator": {
        "user": {
            "userId": 344147139494862,
            "firstName": "Reed",
            "lastName": "Feldman",
            "displayName": "Reed Feldman (SUP)",
            "email": "reed.feldman@symphony.com",
            "username": "reedUAT"
        }
    },
    "payload": {
        "symphonyElementsAction": {
            "stream": {
                "streamId": "IEj12WoWsfTkiqOBkATdUn___pFXhN9OdA",
                "streamType": "IM"
            },
            "formMessageId": "BFawdKkxmV0ZQmSuIzgfTX___oyR5yO2bQ",
            "formId": "form_id",
             "formValues": {
                    "action": "tablesel-row-1"
                }
        }
    }
}

Rules and Limitations

  • The table can be generated without header or footer.

  • If the table type is equal to "checkbox", then checkboxes will be added to the rows of the table.

  • A checkbox can also be added in the header. Please note, clicking the checkbox in the header will not check all rows' checkboxes.

  • If the table type is equal to "button", then buttons will be added to select a specific row of the table.

Timezone Picker

Do you need users to pick a timezone as part of an interactive flow? With the Timezone Picker, Symphony users can easily select a timezone from a selection of possible values. It recognises both cities and countries so the user can quickly filter and find the right timezone.

MessageML tag

The Timezone Picker is represented by the <timezone-picker> tag, as you can see in the examples at the bottom of the page.

Designs

You can see below the designs of the timezone picker.

For a list of all the available elements, refer to Elements.

Attributes

Attribute

Type

Required?

Description

name

String

Yes

Identifies the timezone picker.

value

String

No

Timezone displayed by default when the user receives or resets the form.

Please note that if it is not defined by the developer, the value will be based on the user's browser setting. You need to enter value="" to enforce the timezone to be empty.

title

String (accepting \n for line break)

No

The description that will be displayed when clicking the tooltip icon located on top of the timezone picker Element.

label

String

Not required but it is recommended if title is defined

Definition of the label that will be displayed on top of the timezone picker Element.

required

Boolean (true or false)

No

If true, it specifies that a timezone must be picked by the user before submitting the form.

placeholder

String

No

Specifies a short hint in the timezone picker input. Accepts any string value but is overridden by the value if a value is defined: it will show only if value attribute is enforced to empty.

Please note that a default placeholder is displayed as: "Choose a timezone".

disabled-timezone

Array of Strings

No

Items can be disabled from the timezone picker list so that the user cannot pick them. See the examples below for more details.

Important: single quote ' should wrap the array of strings

Accessibility

For the purpose of accessibility, Symphony users can interact with the timezone picker via their keyboard:

  • First, using "Tab" to enter the component

  • Using "Enter", "Arrow-up", or "Arrow-down" to open the timezone list

  • Using "Arrow-up" and "Arrow-down" to navigate in the list

  • Using any key to erase the field and write in it

  • Using "Enter" to set the selected timezone and close the list

  • Using "Escape" to keep pre-selected value and close the list

  • Finally using "Tab" to exit the component

Rules and Limitations

  • The max length of any timezone picker attribute is 256 characters except disabled-timezone attribute which max length is set to 1024 characters.

  • All timezone values are displayed in English only.

  • You can add a default timezone in your text field by including it in the value parameter. Please note that unlike the placeholder text, the default timezone will be sent in the formReply when the form is submitted if not edited by the user.

  • Please note that if the default timezone (value attribute) matches a value from the disabled-timezone array, then the value is left empty.

  • The timezone-picker will be supported on the following versions of clients:

    • 20.14 for Client 2.0

    • 20.13 for Client 1.5 (a beta version is released in Client 20.12 for Client 1.5)

When the value attribute is not defined by the developer, please note that the default value will be based on the user's browser setting.

If you want however to force the default value to be displayed as empty for the user when sending the form, please enforce value="" in the messageML.

Examples

The following examples show the timezone picker being used as follows:

  • The first timezone-picker (init) shows how to display an empty timezone-picker ("With empty default value"). Note in the messageML sent the value was enforced to empty with value="".

  • The second timezone-picker (specific_value) shows how to display, by default, a value specifically chosen by the developer. Note that the default value would have been sent to the payload if it had not been deleted before submitting the form. You can see how users can remove a pre-selected value from the timezone-picker, thanks to the cross on the right side of the input.

  • The third timezone-picker (default_value) shows how to display, by default, the user browser timezone value. Note that the default value is sent to the payload when submitting the form.

  • The fourth timezone-picker (placeholder) shows how a placeholder text ("Only Placeholder") is displayed in the timezone-picker. Please note the placeholder text is not sent in the payload if no value has been chosen by the enduser.

  • The fifth timezone-picker (label) shows how a label text ("My Label") is displayed.

  • The sixth timezone-picker (tooltip) shows how a title text ("My Tooltip/n With a second line") is inserted in the UI under the (i) icon, and how the text entered in the title parameter is displayed when the enduser clicks on the icon.

  • The seventh timezone-picker (noreq) shows how a user can interact with a non-required field. Even if the field is empty (only a placeholder text is present but does not count as a value), it does not prevent the user from submitting the form.

  • The eighth timezone-picker (req) shows the behavior of the unique required field of the form, which cannot be submitted in case it is not filled; an error is displayed under the field in case the user submits the form with this field as empty.

  • The ninth timezone-picker (disabled) shows how users interact with disabled values from the timezone-picker.

<messageML>
  <form id="form_id">
    <h2>timezone-pickers</h2>
      <timezone-picker name="init" value="" label="With empty default value" />
      <timezone-picker name="specific_value" value="America/Fortaleza" label="With specific default value" />
      <timezone-picker name="default_value" label="With default value from the user browser"/>
      <timezone-picker name="placeholder" placeholder="Only Placeholder..." value="" />
      <timezone-picker name="label" label="My Label" />
      <timezone-picker name="tooltip" label="With Tooltip" title="My Tooltip\n With a second line" />
      <timezone-picker name="noreq" placeholder="Not required" value="" />
      <timezone-picker name="req" required="true" placeholder="Required" value="" />
      <timezone-picker name="disabled" label="With Disabled values" value="America/Indiana/Vincennes" disabled-timezone='["America/Detroit", "America/Indiana/Marengo", "America/Indiana/Petersburg"]' />
      <button name="timezone-picker">Submit</button>
  </form>
</messageML>
[
    {
        "id": "0taiHA",
        "messageId": "6nZt52ETzcAeuTJKukhrxH___ofZ7Yl-bQ",
        "timestamp": 1615546447489,
        "type": "SYMPHONYELEMENTSACTION",
        "initiator": {
            "user": {
                "userId": 7078106482890,
                "firstName": "User",
                "lastName": "Bot",
                "displayName": "User",
                "email": "userbot@symphony.com",
                "username": "user_bot"
            }
        },
        "payload": {
            "symphonyElementsAction": {
                "stream": {
                    "streamId": "tp3eVYR3ZTyW7K7vmsmgYX___oq6-yN5dA",
                    "streamType": "ROOM"
                },
                "formMessageId": "Lj_jvoJ-UFtR3yHS2T3lPn___ofZ9beJbQ",
                "formId": "form_id",
                "formValues": {
                    "action": "timezone-picker",
                    "init": "",
                    "specific_value": "",
                    "default_value": "Europe/Paris",
                    "placeholder": "",
                    "label": "Europe/Paris",
                    "tooltip": "Europe/Paris",
                    "noreq": "",
                    "req": "Europe/London",
                    "disabled": "America/Indiana/Vincennes"
                }
            }
        }
    }
]

Versions and Compatibility

Main features introduced

Agent needed to parse message sent by the bot

Client 2.0 release

Backward client-compatibility behavior (e.g. external rooms)

Beta version

20.12

Not supported

Initial release

20.12

20.14

Not supported (except for 20.12 client 1.5)

Annex: list of timezone values

Please note in the examples above that the values are displayed to users according to the following rules:

  • The country name only is displayed if it has a single timezone (see Israel)

  • A list of cities are displayed under their country title if this country has multiple timezones (see United States of America)

Date Picker

Do you need users to pick a date as part of an interactive flow? With the Date Picker element, Symphony users can easily select a date from a calendar-style UI component.

The Date Picker offers to Symphony users the possibility to enter the date in two different ways:

  • Populating the field directly with the keyboard, following the appropriate format

  • Clicking on the wished day from the calendar UI in a monthly display

MessageML tag

The Date Picker is represented by the <date-picker> tag, as you can see in the examples at the bottom of the page.

Designs

You can see below the designs of the date picker. The main points to be highlighted are the following:

  • A dot is displayed under the today date

  • A disabled date is displayed in grey and stroke through

  • An highlighted date is displayed in blue

For a list of all the available elements, refer to Elements.

Attributes

Attribute

Type

Required?

Description

name

String

Yes

Identifies the date picker.

value

String "yyy-MM-dd"

No

Date with ISO_8601 format to be displayed by default by the date picker when rendered for the Symphony user. Overrides the placeholder value.

title

String (accepting \n for line break)

No

The description that will be displayed when clicking the tooltip icon located on top of the date picker Element.

label

String

Not required but it is recommend if title is defined

Definition of the label that will be displayed on top of the date picker Element.

required

Boolean (true or false)

No

If true, it specifies that a date must be picked before submitting the form.

placeholder

String

No

Specifies a short hint that describes the expected format of the date picker. Accepts any string value but is overridden by the value if a value is entered. If not set, the accepted format will be displayed. Note: be careful as adding a placeholder might bring confusion to the end user. Therefore we recommend to use the default one as much as possible, to use the label if context is needed, or the title if instructions are needed.

min

String "yyy-MM-dd"

No

Specifies the earliest acceptable date with ISO_8601 format.

max

String "yyy-MM-dd"

No

Specifies the latest acceptable date with ISO_8601 format.

disabled-date

Json array (in a String format)

No

Dates or ranges of dates that cannot be selected by the Symphony user. Maximum length of 1024 characters. There are 3 different patterns: 1. range of date: {"from": "yyyy-MM-dd", "to": "yyyy-MM-dd"}, 2. date: {"day": "yyyy-MM-dd"}, 3. recurring day in the week: {"daysOfWeek": [0, 1, 6]}, Important: single quote ' should wrap the xml parameter. See the examples below for more details. Note: for the daysOfWeek: 0 always corresponds to Sunday, 6 to Saturday. See the examples below for more details.

highlighted-date

Json array (in a String format)

No

Dates or ranges of dates that are highlighted to the Symphony user. Maximum length of 1024 characters. There are 3 different patterns: 1. range of date: {"from": "yyyy-MM-dd", "to": "yyyy-MM-dd"}, 2. date: {"day": "yyyy-MM-dd"}, 3. recurring day in the week: {"daysOfWeek": [0, 1, 6]}, Important: single quote ' should wrap the xml parameter. See the examples below for more details. Note: for the daysOfWeek: 0 always corresponds to Sunday, 6 to Saturday. See the examples below for more details.

format

String

No

Format in which the date will be displayed to or should be entered by the Symphony user. Only accepts the letters 'y', 'M', and 'd'. • 'd' defines the day of the month. It can be either 'd' or 'dd' to define the minimum of digits displayed (i.e. corresponding to "3" or "03" for the third day) • 'M' defines the month. Use one or two ('M' or 'MM') for the numerical month; three 'MMM' for the abbreviation (i.e. "Sep"); four 'MMMM' for the full name (i.e. "September"); or five 'MMMMM' for the narrow name (i.e. "S") • 'y' defines the year. Use n of it to define n minimum of digits displayed (i.e. use 'yyyyy' to display the year 2020 as 02020) Note: be careful as many combinations may be possible, may have a weird display for the Symphony user, and are not restricted in the messageML. Therefore, we strongly recommend to use the default value as much as possible. Note 2: The format only impacts what the end user will see. It does not affect how you have to specify the value, disabled-date, highlighted-date, or the format of the response.

Accessibility

For the purpose of accessibility, Symphony users can interact with the calendar UI with their keyboard:

  • Using "Enter" to open/close the calendar UI

  • Then using "Tab" to navigate in the different functionalities of the calendar: day selection; then "today" button; then "previous year" button; then "previous month" button; then "next month" button; then "next year" button; and finally going back to the day selection panel

  • Once in the day selection area, using the arrows from the keyboard allows navigating in the month to select the right day

  • Finally, using "Enter" allows to close the panel with the day selected or to trigger one of the functionalities selected (buttons)

In the day selection area, some more keyboard controls are available in the date picker:

  • "Home" key: Moves focus to the first day (e.g. Sunday) of the current week

  • "End" key: Moves focus to the last day (e.g. Saturday) of the current week

  • "Page Up" key: Moves to the previous month, with focus on the same day of the same week (or either previous or next week if it does not exist)

  • "Page Down" key: Moves to the next month, with focus on the same day of the same week (or either previous or next week if it does not exist)

  • "Shift" + "Page Up" key: Moves to the previous year, with focus on the same day of the same week (or either previous or next week if it does not exist)

  • "Shift" + "Page Down" key: Moves to the next year, with focus on the same day of the same week (or either previous or next week if it does not exist)

Rules and Limitations

  • The max length of any date picker attribute is 256 except disabled-date and highlighted-date attributes which max length is set to 1024 characters.

  • The date picker is displayed differently to the end user depending on the language chosen in the settings specific of each user. Please note that if a format is specified, then the format overrides the default format chosen based on the settings.

    • English: the calendar-UI is in English, the weeks start on Sunday, the default format is MM-dd-yyyy, and the date picker accepts value to be entered with the keyboard in English only (i.e. August)

    • French: the calendar-UI is in French, the weeks start on Monday, the default format is dd/MM/yyyy, and the date picker accepts value to be entered with the keyboard in French only (i.e. Août)

    • Japanese: the calendar-UI is in Japanese, the weeks start on Monday, the default format is yyyy日MM月dd年, and the date picker accepts value to be entered with the keyboard in Japanese only

  • If the user enters the value of a valid day and month combination with his keyboard, then the current year is preselected. If he continues entering another year, then this last one will be selected.

  • If the format entered by the user is not correct or if the date entered is a disabled date, then an error message is displayed to the user. Note that it is not possible for the user to submit the form with an invalid format or disabled date.

  • You can add a default date in your text field by including it in the value parameter. Please note that unlike the placeholder text, the default date will be sent in the formReply when the form is submitted if not edited by the user.

Date as disabled and highlighted

Note that currently, if a date is entered in the disabled and highlighted parameters at the same time, it cannot be selected via the calendar UI first. However, it can be enabled if the user enters this particular date manually. This behaviour will be fixed in the next versions and the date will then be considered as disabled and displayed as such.

Examples

The following examples show the date picker being used as follows:

  • The first date-picker (init) shows how to display a default date, as an initial value is entered as parameter. Note that the default text will be sent to the payload given that it was not deleted before submitting the form.

  • The second date-picker (placeholder) shows how a placeholder is displayed in the UI. Please note that any text is accepted as input. However, if you compare with the next date-pickers present in the form, you can notice that a default placeholder is generated (with a hint of the correct format to accepted by the date-picker field) in case no placeholder is mentioned in parameter.

  • The third date-picker (label) shows how a label is displayed. In the GIF below, it shows also the interaction with the form when the user directly writes an input with an incorrect format: an error message is displayed and the form cannot be sent.

  • The fourth date-picker (tooltip) shows how the title attribute is displayed. Note that when a user enters the month and the date in the correct format, the current year is automatically preselected. It can be modified if the user continues writing the year in the input field.

  • The fifth date-picker (req) shows the behaviour of the unique required field of the form, which cannot be submitted in case it is not filled. Also note the use of the "today" button.

  • The sixth date-picker (format) shows the way the placeholder evolves to adapt to a new format transmitted thanks to the format parameter. Also please note the accessible interaction with the date-picker via the keyboard.

  • The seventh date-picker (rules) shows how to interact with the following parameters: min, max, disabled, and highlighted. Please note that a disabled date cannot be entered manually.

<messageML>
  <form id="form_id">
    <h2>date-pickers</h2>
      <date-picker name="init" value="2020-08-28"></date-picker>
      <date-picker name="placeholder" placeholder="Only Placeholder: MM-DD-YYYY"></date-picker>
      <date-picker name="label" label="My Label"></date-picker>
      <date-picker name="tooltip" label="With Tooltip" title="My Tooltip\n With a second line"></date-picker>
      <date-picker name="req" required="true" label="Required"></date-picker>
      <date-picker name="format" label="Other Format" format="dd/MM/yyyy"></date-picker>
      <date-picker name="rules" label="With disabled and highlighted dates" min="2021-01-04" max="2021-01-27" disabled-date='[{"from": "2021-01-13", "to": "2021-01-15"}, {"from": "2021-01-19", "to": "2021-01-21"}, {"day": "2021-01-06"}, {"daysOfWeek": [0,6]}]' highlighted-date='[{"day": "2021-01-05"}, {"day": "2021-01-26"}]' />
      <button name="date-picker">Submit</button>
  </form>
</messageML>
[
    {
        "id": "iMpXPu",
        "messageId": "q9zkRlpFCYtzPiJ1aoCuWH___okHkL4TbQ",
        "timestamp": 1610485809644,
        "type": "SYMPHONYELEMENTSACTION",
        "initiator": {
            "user": {
                "userId": 7078106482890,
                "firstName": "User",
                "lastName": "Bot",
                "displayName": "User",
                "email": "userbot@symphony.com",
                "username": "user_bot"
            }
        },
        "payload": {
            "symphonyElementsAction": {
                "stream": {
                    "streamId": "iiI-kbayOnMXCO-rb4hDN3___orZLc2XdA",
                    "streamType": "IM"
                },
                "formMessageId": "GZQEfj3SQ6UF_kPuqXo3eH___okIkJySbQ",
                "formId": "form_id",
                "formValues": {
                    "action": "date-picker",
                    "init": "2020-08-28",
                    "placeholder": "",
                    "label": "2021-01-16",
                    "tooltip": "2021-01-15",
                    "req": "2021-01-12",
                    "format": "2020-02-29",
                    "rules": "2021-01-05"
                }
            }
        }
    }
]

Versions and Compatibility

Main features introduced

Agent needed to parse message sent by the bot

Client 2.0 release

Backward client-compatibility behavior (e.g. external rooms)

Initial release

20.12

20.12

Not working: date-picker is not displayed but the form can be sent

Emojis

Chat bots can send the same emojis as end users thanks to the MessageML short form tag <emoji shortcode="code" />.

Example

Here is an example of a message containing emojis:

Annex: list of emoji codes

The following table shows the codes corresponding to their emoji symbol:

Code

Emoji

100

💯

1234

🔢

+1

👍

-1

👎

8ball

🎱

a

🅰

ab

🆎

abc

🔤

abcd

🔡

accept

🉑

aerial_tramway

🚡

airplane

✈

alarm_clock

⏰

alien

👽

ambulance

🚑

anchor

⚓

angel

👼

anger

💢

angry

😠

anguished

😧

ant

🐜

apple

🍎

aquarius

♒

aries

♈

arrow_backward

◀

arrow_double_down

⏬

arrow_double_up

⏫

arrow_down

⬇

arrow_down_small

🔽

arrow_forward

▶

arrow_heading_down

⤵

arrow_heading_up

⤴

arrow_left

⬅

arrow_lower_left

↙

arrow_lower_right

↘

arrow_right

➡

arrow_right_hook

↪

arrow_up

⬆

arrow_up_down

↕

arrow_up_small

🔼

arrow_upper_left

↖

arrow_upper_right

↗

arrows_clockwise

🔃

arrows_counterclockwise

🔄

art

🎨

articulated_lorry

🚛

astonished

😲

athletic_shoe

👟

atm

🏧

b

🅱

baby

👶

baby_bottle

🍼

baby_chick

🐤

baby_symbol

🚼

back

🔙

baggage_claim

🛄

balloon

🎈

ballot_box_with_check

☑

bamboo

🎍

bangbang

‼

bank

🏦

bar_chart

📊

barber

💈

baseball

⚾

basketball

🏀

bath

🛀

bathtub

🛁

battery

🔋

bear

🐻

bee

🐝

beer

🍺

beers

🍻

beetle

🐞

beginner

🔰

bell

🔔

bento

🍱

bicyclist

🚴

bike

🚲

bikini

👙

bird

🐦

birthday

🎂

black_circle

⚫

black_joker

🃏

black_large_square

⬛

black_medium_small_square

◾

black_medium_square

◼

black_nib

✒

black_small_square

▪

black_square_button

🔲

blossom

🌼

blowfish

🐡

blue_book

📘

blue_car

🚙

blue_heart

💙

blush

😊

boar

🐗

boat

⛵

bomb

💣

book

📖

bookmark

🔖

bookmark_tabs

📑

books

📚

boom

💥

boot

👢

bouquet

💐

bow

🙇

bowling

🎳

boy

👦

bread

🍞

bride_with_veil

👰

bridge_at_night

🌉

briefcase

💼

broken_heart

💔

bug

🐛

bulb

💡

bullettrain_front

🚅

bullettrain_side

🚄

bus

🚌

busstop

🚏

bust_in_silhouette

👤

busts_in_silhouette

👥

cactus

🌵

cake

🍰

calendar

📆

calling

📲

camel

🐫

camera

📷

cancer

♋

candy

🍬

capital_abcd

🔠

capricorn

♑

car

🚗

card_index

📇

carousel_horse

🎠

cat

🐱

cat2

🐈

cd

💿

chart

💹

chart_with_downwards_trend

📉

chart_with_upwards_trend

📈

checkered_flag

🏁

cherry_blossom

🌸

chestnut

🌰

chicken

🐔

children_crossing

🚸

chocolate_bar

🍫

christmas_tree

🎄

church

⛪

cinema

🎦

circus_tent

🎪

city_sunrise

🌇

city_sunset

🌆

cl

🆑

clap

👏

clapper

🎬

clipboard

📋

clock1

🕐

clock10

🕙

clock1030

🕥

clock11

🕚

clock1130

🕦

clock12

🕛

clock1230

🕧

clock130

🕜

clock2

🕑

clock230

🕝

clock3

🕒

clock330

🕞

clock4

🕓

clock430

🕟

clock5

🕔

clock530

🕠

clock6

🕕

clock630

🕡

clock7

🕖

clock730

🕢

clock8

🕗

clock830

🕣

clock9

🕘

clock930

🕤

closed_book

📕

closed_lock_with_key

🔐

closed_umbrella

🌂

cloud

☁

clubs

♣

cocktail

🍸

coffee

☕

cold_sweat

😰

collision

💥

computer

💻

confetti_ball

🎊

confounded

😖

confused

😕

congratulations

㊗

construction

🚧

construction_worker

👷

convenience_store

🏪

cookie

🍪

cool

🆒

cop

👮

copyright

©

corn

🌽

couple

👫

couple_with_heart

💑

couplekiss

💏

cow

🐮

cow2

🐄

credit_card

💳

crescent_moon

🌙

crocodile

🐊

crossed_flags

🎌

crown

👑

cry

😢

crying_cat_face

😿

crystal_ball

🔮

cupid

💘

curly_loop

➰

currency_exchange

💱

curry

🍛

custard

🍮

customs

🛃

cyclone

🌀

dancer

💃

dancers

👯

dango

🍡

dart

🎯

dash

💨

date

📅

deciduous_tree

🌳

department_store

🏬

diamond_shape_with_a_dot_inside

💠

diamonds

♦

disappointed

😞

disappointed_relieved

😥

dizzy

💫

dizzy_face

😵

do_not_litter

🚯

dog

🐶

dog2

🐕

dollar

💵

dolls

🎎

dolphin

🐬

door

🚪

dragon

🐉

dragon_face

🐲

dress

👗

dromedary_camel

🐪

droplet

💧

dvd

📀

e-mail

📧

ear

👂

ear_of_rice

🌾

earth_africa

🌍

earth_americas

🌎

earth_asia

🌏

egg

🍳

eight_pointed_black_star

✴

eight_spoked_asterisk

✳

electric_plug

🔌

elephant

🐘

email

✉

end

🔚

envelope

✉

envelope_with_arrow

📩

euro

💶

european_castle

🏰

european_post_office

🏤

evergreen_tree

🌲

exclamation

❗

expressionless

😑

eyeglasses

👓

eyes

👀

facepunch

👊

factory

🏭

fallen_leaf

🍂

family

👪

fast_forward

⏩

fax

📠

fearful

😨

feet

🐾

ferris_wheel

🎡

file_folder

📁

fire

🔥

fire_engine

🚒

fireworks

🎆

first_quarter_moon

🌓

first_quarter_moon_with_face

🌛

fish

🐟

fish_cake

🍥

fishing_pole_and_fish

🎣

fist

✊

flags

🎏

flashlight

🔦

floppy_disk

💾

flower_playing_cards

🎴

flushed

😳

foggy

🌁

football

🏈

footprints

👣

fork_and_knife

🍴

fountain

⛲

four_leaf_clover

🍀

free

🆓

fried_shrimp

🍤

fries

🍟

frog

🐸

frowning

😦

fuelpump

⛽

full_moon

🌕

full_moon_with_face

🌝

game_die

🎲

gem

💎

gemini

♊

ghost

👻

gift

🎁

gift_heart

💝

girl

👧

globe_with_meridians

🌐

goat

🐐

golf

⛳

grapes

🍇

green_apple

🍏

green_book

📗

green_heart

💚

grey_exclamation

❕

grey_question

❔

grimacing

😬

grin

😁

grinning

😀

guardsman

💂

guitar

🎸

gun

🔫

haircut

💇

hamburger

🍔

hammer

🔨

hamster

🐹

hand

✋

handbag

👜

handshake

hankey

💩

hatched_chick

🐥

hatching_chick

🐣

headphones

🎧

heart

❤

heart_decoration

💟

heart_eyes

😍

heart_eyes_cat

😻

heartbeat

💓

heartpulse

💗

hearts

♥

heavy_check_mark

✔

heavy_division_sign

➗

heavy_dollar_sign

💲

heavy_exclamation_mark

❗

heavy_minus_sign

➖

heavy_multiplication_x

✖

heavy_plus_sign

➕

helicopter

🚁

herb

🌿

hibiscus

🌺

high_brightness

🔆

high_heel

👠

honey_pot

🍯

honeybee

🐝

horse

🐴

horse_racing

🏇

hospital

🏥

hotel

🏨

hotsprings

♨

hourglass

⌛

hourglass_flowing_sand

⏳

house

🏠

house_with_garden

🏡

hushed

😯

ice_cream

🍨

icecream

🍦

id

🆔

ideograph_advantage

🉐

imp

👿

inbox_tray

📥

incoming_envelope

📨

information_desk_person

💁

information_source

ℹ

innocent

😇

interrobang

⁉

iphone

📱

izakaya_lantern

🏮

jack_o_lantern

🎃

japan

🗾

japanese_castle

🏯

japanese_goblin

👺

japanese_ogre

👹

jeans

👖

joy

😂

joy_cat

😹

key

🔑

keycap_ten

🔟

kimono

👘

kiss

💋

kissing

😗

kissing_cat

😽

kissing_closed_eyes

😚

kissing_heart

😘

kissing_smiling_eyes

😙

koala

🐨

koko

🈁

lantern

🏮

large_blue_circle

🔵

large_blue_diamond

🔷

large_orange_diamond

🔶

last_quarter_moon

🌗

last_quarter_moon_with_face

🌜

laughing

😆

leaves

🍃

ledger

📒

left_luggage

🛅

left_right_arrow

↔

leftwards_arrow_with_hook

↩

lemon

🍋

leo

♌

leopard

🐆

libra

♎

light_rail

🚈

link

🔗

lips

👄

lipstick

💄

lock

🔒

lock_with_ink_pen

🔏

lollipop

🍭

loop

➿

loudspeaker

📢

love_hotel

🏩

love_letter

💌

low_brightness

🔅

m

Ⓜ

mag

🔍

mag_right

🔎

mahjong

🀄

mailbox

📫

mailbox_closed

📪

mailbox_with_mail

📬

mailbox_with_no_mail

📭

man

👨

man_with_gua_pi_mao

👲

man_with_turban

👳

mans_shoe

👞

maple_leaf

🍁

mask

😷

massage

💆

meat_on_bone

🍖

mega

📣

melon

🍈

memo

📝

mens

🚹

metro

🚇

microphone

🎤

microscope

🔬

milky_way

🌌

minibus

🚐

minidisc

💽

mobile_phone_off

📴

money_with_wings

💸

moneybag

💰

monkey

🐒

monkey_face

🐵

monorail

🚝

moon

🌔

mortar_board

🎓

mount_fuji

🗻

mountain_bicyclist

🚵

mountain_cableway

🚠

mountain_railway

🚞

mouse

🐭

mouse2

🐁

movie_camera

🎥

moyai

🗿

muscle

💪

mushroom

🍄

musical_keyboard

🎹

musical_note

🎵

musical_score

🎼

mute

🔇

nail_care

💅

name_badge

📛

necktie

👔

negative_squared_cross_mark

❎

neutral_face

😐

new

🆕

new_moon

🌑

new_moon_with_face

🌚

newspaper

📰

ng

🆖

no_bell

🔕

no_bicycles

🚳

no_entry

⛔

no_entry_sign

🚫

no_good

🙅

no_mobile_phones

📵

no_mouth

😶

no_pedestrians

🚷

no_smoking

🚭

non-potable_water

🚱

nose

👃

notebook

📓

notebook_with_decorative_cover

📔

notes

🎶

nut_and_bolt

🔩

o

⭕

o2

🅾

ocean

🌊

octopus

🐙

oden

🍢

office

🏢

ok

🆗

ok_hand

👌

ok_woman

🙆

older_man

👴

older_woman

👵

on

🔛

oncoming_automobile

🚘

oncoming_bus

🚍

oncoming_police_car

🚔

oncoming_taxi

🚖

open_book

📖

open_file_folder

📂

open_hands

👐

open_mouth

😮

ophiuchus

⛎

orange_book

📙

outbox_tray

📤

ox

🐂

package

📦

page_facing_up

📄

page_with_curl

📃

pager

📟

palm_tree

🌴

panda_face

🐼

paperclip

📎

parking

🅿

part_alternation_mark

〽

partly_sunny

⛅

passport_control

🛂

paw_prints

🐾

pear

🍐

pencil

📝

pencil2

✏

penguin

🐧

pensive

😔

performing_arts

🎭

persevere

😣

person_frowning

🙍

person_with_blond_hair

👱

person_with_pouting_face

🙎

phone

☎

pig

🐷

pig2

🐖

pig_nose

🐽

pill

💊

pisces

♓

pizza

🍕

point_down

👇

point_left

👈

point_right

👉

point_up

☝

point_up_2

👆

police_car

🚓

poodle

🐩

post_office

🏣

postal_horn

📯

postbox

📮

potable_water

🚰

pouch

👝

poultry_leg

🍗

pound

💷

pouting_cat

😾

pray

🙏

princess

👸

punch

👊

purple_heart

💜

purse

👛

pushpin

📌

put_litter_in_its_place

🚮

question

❓

rabbit

🐰

rabbit2

🐇

racehorse

🐎

radio

📻

radio_button

🔘

rage

😡

railway_car

🚃

rainbow

🌈

raised_hand

✋

raised_hands

🙌

raising_hand

🙋

ram

🐏

ramen

🍜

rat

🐀

recycle

♻

red_car

🚗

red_circle

🔴

registered

®

relaxed

☺

relieved

😌

repeat

🔁

repeat_one

🔂

restroom

🚻

revolving_hearts

💞

rewind

⏪

ribbon

🎀

rice

🍚

rice_ball

🍙

rice_cracker

🍘

rice_scene

🎑

ring

💍

rocket

🚀

roller_coaster

🎢

rooster

🐓

rose

🌹

rotating_light

🚨

round_pushpin

📍

rowboat

🚣

rugby_football

🏉

runner

🏃

running

🏃

running_shirt_with_sash

🎽

sa

🈂

sagittarius

♐

sailboat

⛵

sake

🍶

sandal

👡

santa

🎅

satellite

📡

satisfied

😆

saxophone

🎷

school

🏫

school_satchel

🎒

scissors

✂

scorpius

♏

scream

😱

scream_cat

🙀

scroll

📜

seat

💺

secret

㊙

seedling

🌱

shaved_ice

🍧

sheep

🐑

shell

🐚

ship

🚢

shirt

👕

shoe

👞

shower

🚿

signal_strength

📶

six_pointed_star

🔯

ski

🎿

skull

💀

sleeping

😴

sleepy

😪

slot_machine

🎰

small_blue_diamond

🔹

small_orange_diamond

🔸

small_red_triangle

🔺

small_red_triangle_down

🔻

smile

😄

smile_cat

😸

smiley

😃

smiley_cat

😺

smiling_imp

😈

smirk

😏

smirk_cat

😼

smoking

🚬

snail

🐌

snake

🐍

snowboarder

🏂

snowflake

❄

snowman

⛄

sob

😭

soccer

⚽

soon

🔜

sos

🆘

sound

🔉

space_invader

👾

spades

♠

spaghetti

🍝

sparkle

❇

sparkler

🎇

sparkles

✨

sparkling_heart

💖

speaker

🔊

speech_balloon

💬

speedboat

🚤

star

⭐

star2

🌟

stars

🌃

station

🚉

statue_of_liberty

🗽

steam_locomotive

🚂

stew

🍲

straight_ruler

📏

stuck_out_tongue

😛

stuck_out_tongue_closed_eyes

😝

stuck_out_tongue_winking_eye

😜

sun_with_face

🌞

sunflower

🌻

sunglasses

😎

sunny

☀

sunrise

🌅

sunrise_over_mountains

🌄

surfer

🏄

sushi

🍣

suspension_railway

🚟

sweat

😓

sweat_smile

😅

sweet_potato

🍠

swimmer

🏊

symbols

🔣

syringe

💉

tada

🎉

tanabata_tree

🎋

tangerine

🍊

taurus

♉

taxi

🚕

tea

🍵

telephone

☎

telephone_receiver

📞

telescope

🔭

tennis

🎾

tent

⛺

thought_balloon

💭

thumbsdown

👎

thumbsup

👍

ticket

🎫

tiger

🐯

tiger2

🐅

tired_face

😫

tm

™

toilet

🚽

tokyo_tower

🗼

tomato

🍅

tongue

👅

top

🔝

tophat

🎩

tractor

🚜

traffic_light

🚥

train

🚃

train2

🚆

tram

🚊

triangular_flag_on_post

🚩

triangular_ruler

📐

trident

🔱

triumph

😤

trolleybus

🚎

trophy

🏆

tropical_drink

🍹

tropical_fish

🐠

truck

🚚

trumpet

🎺

tshirt

👕

tulip

🌷

turtle

🐢

tv

📺

twisted_rightwards_arrows

🔀

two_hearts

💕

two_men_holding_hands

👬

two_women_holding_hands

👭

u5272

🈹

u5408

🈴

u55b6

🈺

u6307

🈯

u6708

🈷

u6709

🈶

u6e80

🈵

u7121

🈚

u7533

🈸

u7981

🈲

u7a7a

🈳

umbrella

☔

unamused

😒

underage

🔞

unlock

🔓

up

🆙

v

✌

vertical_traffic_light

🚦

vhs

📼

vibration_mode

📳

video_camera

📹

video_game

🎮

violin

🎻

virgo

♍

volcano

🌋

vs

🆚

walking

🚶

waning_crescent_moon

🌘

waning_gibbous_moon

🌖

warning

⚠

watch

⌚

water_buffalo

🐃

watermelon

🍉

wave

👋

wavy_dash

〰

waxing_crescent_moon

🌒

waxing_gibbous_moon

🌔

wc

🚾

weary

😩

wedding

💒

whale

🐳

whale2

🐋

wheelchair

♿

white_check_mark

✅

white_circle

⚪

white_flower

💮

white_large_square

⬜

white_medium_small_square

◽

white_medium_square

◻

white_small_square

▫

white_square_button

🔳

wind_chime

🎐

wine_glass

🍷

wink

😉

wolf

🐺

woman

👩

womans_clothes

👚

womans_hat

👒

womens

🚺

worried

😟

wrench

🔧

x

❌

yellow_heart

💛

yen

💴

yum

😋

zap

⚡

zzz

💤

angel_tone1

👼🏻

angel_tone2

👼🏼

angel_tone3

👼🏽

angel_tone4

👼🏾

angel_tone5

👼🏿

asterisk

*⃣

baby_tone1

👶🏻

baby_tone2

👶🏼

baby_tone3

👶🏽

baby_tone4

👶🏾

baby_tone5

👶🏿

basketball_player_tone1

⛹🏻

basketball_player_tone2

⛹🏼

basketball_player_tone3

⛹🏽

basketball_player_tone4

⛹🏾

basketball_player_tone5

⛹🏿

bath_tone1

🛀🏻

bath_tone2

🛀🏼

bath_tone3

🛀🏽

bath_tone4

🛀🏾

bath_tone5

🛀🏿

bicyclist_tone1

🚴🏻

bicyclist_tone2

🚴🏼

bicyclist_tone3

🚴🏽

bicyclist_tone4

🚴🏾

bicyclist_tone5

🚴🏿

bow_tone1

🙇🏻

bow_tone2

🙇🏼

bow_tone3

🙇🏽

bow_tone4

🙇🏾

bow_tone5

🙇🏿

boy_tone1

👦🏻

boy_tone2

👦🏼

boy_tone3

👦🏽

boy_tone4

👦🏾

boy_tone5

👦🏿

bride_with_veil_tone1

👰🏻

bride_with_veil_tone2

👰🏼

bride_with_veil_tone3

👰🏽

bride_with_veil_tone4

👰🏾

bride_with_veil_tone5

👰🏿

call_me_tone1

🤙🏻

call_me_tone2

🤙🏼

call_me_tone3

🤙🏽

call_me_tone4

🤙🏾

call_me_tone5

🤙🏿

cartwheel_tone1

🤸🏻

cartwheel_tone2

🤸🏼

cartwheel_tone3

🤸🏽

cartwheel_tone4

🤸🏾

cartwheel_tone5

🤸🏿

clap_tone1

👏🏻

clap_tone2

👏🏼

clap_tone3

👏🏽

clap_tone4

👏🏾

clap_tone5

👏🏿

cn

🇨🇳

construction_worker_tone1

👷🏻

construction_worker_tone2

👷🏼

construction_worker_tone3

👷🏽

construction_worker_tone4

👷🏾

construction_worker_tone5

👷🏿

cop_tone1

👮🏻

cop_tone2

👮🏼

cop_tone3

👮🏽

cop_tone4

👮🏾

cop_tone5

👮🏿

couple_mm

👨❤👨

couple_ww

👩❤👩

dancer_tone1

💃🏻

dancer_tone2

💃🏼

dancer_tone3

💃🏽

dancer_tone4

💃🏾

dancer_tone5

💃🏿

de

🇩🇪

ear_tone1

👂🏻

ear_tone2

👂🏼

ear_tone3

👂🏽

ear_tone4

👂🏾

ear_tone5

👂🏿

eight

8️⃣

es

🇪🇸

eye_in_speech_bubble

👁🗨

face_palm_tone1

🤦🏻

face_palm_tone2

🤦🏼

face_palm_tone3

🤦🏽

face_palm_tone4

🤦🏾

face_palm_tone5

🤦🏿

family_mmb

👨👨👦

family_mmbb

👨👨👦👦

family_mmg

👨👨👧

family_mmgb

👨👨👧👦

family_mmgg

👨👨👧👧

family_mwbb

👨👩👦👦

family_mwg

👨👩👧

family_mwgb

👨👩👧👦

family_mwgg

👨👩👧👧

family_wwb

👩👩👦

family_wwbb

👩👩👦👦

family_wwg

👩👩👧

family_wwgb

👩👩👧👦

family_wwgg

👩👩👧👧

fingers_crossed_tone1

🤞🏻

fingers_crossed_tone2

🤞🏼

fingers_crossed_tone3

🤞🏽

fingers_crossed_tone4

🤞🏾

fingers_crossed_tone5

🤞🏿

fist_tone1

✊🏻

fist_tone2

✊🏼

fist_tone3

✊🏽

fist_tone4

✊🏾

fist_tone5

✊🏿

five

5️⃣

flag_ac

🇦🇨

flag_ad

🇦🇩

flag_ae

🇦🇪

flag_af

🇦🇫

flag_ag

🇦🇬

flag_ai

🇦🇮

flag_al

🇦🇱

flag_am

🇦🇲

flag_ao

🇦🇴

flag_aq

🇦🇶

flag_ar

🇦🇷

flag_as

🇦🇸

flag_at

🇦🇹

flag_au

🇦🇺

flag_aw

🇦🇼

flag_ax

🇦🇽

flag_az

🇦🇿

flag_ba

🇧🇦

flag_bb

🇧🇧

flag_bd

🇧🇩

flag_be

🇧🇪

flag_bf

🇧🇫

flag_bg

🇧🇬

flag_bh

🇧🇭

flag_bi

🇧🇮

flag_bj

🇧🇯

flag_bl

🇧🇱

flag_bm

🇧🇲

flag_bn

🇧🇳

flag_bo

🇧🇴

flag_bq

🇧🇶

flag_br

🇧🇷

flag_bs

🇧🇸

flag_bt

🇧🇹

flag_bv

🇧🇻

flag_bw

🇧🇼

flag_by

🇧🇾

flag_bz

🇧🇿

flag_ca

🇨🇦

flag_cc

🇨🇨

flag_cd

🇨🇩

flag_cf

🇨🇫

flag_cg

🇨🇬

flag_ch

🇨🇭

flag_ci

🇨🇮

flag_ck

🇨🇰

flag_cl

🇨🇱

flag_cm

🇨🇲

flag_cn

🇨🇳

flag_co

🇨🇴

flag_cp

🇨🇵

flag_cr

🇨🇷

flag_cu

🇨🇺

flag_cv

🇨🇻

flag_cw

🇨🇼

flag_cx

🇨🇽

flag_cy

🇨🇾

flag_cz

🇨🇿

flag_de

🇩🇪

flag_dg

🇩🇬

flag_dj

🇩🇯

flag_dk

🇩🇰

flag_dm

🇩🇲

flag_do

🇩🇴

flag_dz

🇩🇿

flag_ea

🇪🇦

flag_ec

🇪🇨

flag_ee

🇪🇪

flag_eg

🇪🇬

flag_eh

🇪🇭

flag_er

🇪🇷

flag_es

🇪🇸

flag_et

🇪🇹

flag_eu

🇪🇺

flag_fi

🇫🇮

flag_fj

🇫🇯

flag_fk

🇫🇰

flag_fm

🇫🇲

flag_fo

🇫🇴

flag_fr

🇫🇷

flag_ga

🇬🇦

flag_gb

🇬🇧

flag_gd

🇬🇩

flag_ge

🇬🇪

flag_gf

🇬🇫

flag_gg

🇬🇬

flag_gh

🇬🇭

flag_gi

🇬🇮

flag_gl

🇬🇱

flag_gm

🇬🇲

flag_gn

🇬🇳

flag_gp

🇬🇵

flag_gq

🇬🇶

flag_gr

🇬🇷

flag_gs

🇬🇸

flag_gt

🇬🇹

flag_gu

🇬🇺

flag_gw

🇬🇼

flag_gy

🇬🇾

flag_hk

🇭🇰

flag_hm

🇭🇲

flag_hn

🇭🇳

flag_hr

🇭🇷

flag_ht

🇭🇹

flag_hu

🇭🇺

flag_ic

🇮🇨

flag_id

🇮🇩

flag_ie

🇮🇪

flag_il

🇮🇱

flag_im

🇮🇲

flag_in

🇮🇳

flag_io

🇮🇴

flag_iq

🇮🇶

flag_ir

🇮🇷

flag_is

🇮🇸

flag_it

🇮🇹

flag_je

🇯🇪

flag_jm

🇯🇲

flag_jo

🇯🇴

flag_jp

🇯🇵

flag_ke

🇰🇪

flag_kg

🇰🇬

flag_kh

🇰🇭

flag_ki

🇰🇮

flag_km

🇰🇲

flag_kn

🇰🇳

flag_kp

🇰🇵

flag_kr

🇰🇷

flag_kw

🇰🇼

flag_ky

🇰🇾

flag_kz

🇰🇿

flag_la

🇱🇦

flag_lb

🇱🇧

flag_lc

🇱🇨

flag_li

🇱🇮

flag_lk

🇱🇰

flag_lr

🇱🇷

flag_ls

🇱🇸

flag_lt

🇱🇹

flag_lu

🇱🇺

flag_lv

🇱🇻

flag_ly

🇱🇾

flag_ma

🇲🇦

flag_mc

🇲🇨

flag_md

🇲🇩

flag_me

🇲🇪

flag_mf

🇲🇫

flag_mg

🇲🇬

flag_mh

🇲🇭

flag_mk

🇲🇰

flag_ml

🇲🇱

flag_mm

🇲🇲

flag_mn

🇲🇳

flag_mo

🇲🇴

flag_mp

🇲🇵

flag_mq

🇲🇶

flag_mr

🇲🇷

flag_ms

🇲🇸

flag_mt

🇲🇹

flag_mu

🇲🇺

flag_mv

🇲🇻

flag_mw

🇲🇼

flag_mx

🇲🇽

flag_my

🇲🇾

flag_mz

🇲🇿

flag_na

🇳🇦

flag_nc

🇳🇨

flag_ne

🇳🇪

flag_nf

🇳🇫

flag_ng

🇳🇬

flag_ni

🇳🇮

flag_nl

🇳🇱

flag_no

🇳🇴

flag_np

🇳🇵

flag_nr

🇳🇷

flag_nu

🇳🇺

flag_nz

🇳🇿

flag_om

🇴🇲

flag_pa

🇵🇦

flag_pe

🇵🇪

flag_pf

🇵🇫

flag_pg

🇵🇬

flag_ph

🇵🇭

flag_pk

🇵🇰

flag_pl

🇵🇱

flag_pm

🇵🇲

flag_pn

🇵🇳

flag_pr

🇵🇷

flag_ps

🇵🇸

flag_pt

🇵🇹

flag_pw

🇵🇼

flag_py

🇵🇾

flag_qa

🇶🇦

flag_re

🇷🇪

flag_ro

🇷🇴

flag_rs

🇷🇸

flag_ru

🇷🇺

flag_rw

🇷🇼

flag_sa

🇸🇦

flag_sb

🇸🇧

flag_sc

🇸🇨

flag_sd

🇸🇩

flag_se

🇸🇪

flag_sg

🇸🇬

flag_sh

🇸🇭

flag_si

🇸🇮

flag_sj

🇸🇯

flag_sk

🇸🇰

flag_sl

🇸🇱

flag_sm

🇸🇲

flag_sn

🇸🇳

flag_so

🇸🇴

flag_sr

🇸🇷

flag_ss

🇸🇸

flag_st

🇸🇹

flag_sv

🇸🇻

flag_sx

🇸🇽

flag_sy

🇸🇾

flag_sz

🇸🇿

flag_ta

🇹🇦

flag_tc

🇹🇨

flag_td

🇹🇩

flag_tf

🇹🇫

flag_tg

🇹🇬

flag_th

🇹🇭

flag_tj

🇹🇯

flag_tk

🇹🇰

flag_tl

🇹🇱

flag_tm

🇹🇲

flag_tn

🇹🇳

flag_to

🇹🇴

flag_tr

🇹🇷

flag_tt

🇹🇹

flag_tv

🇹🇻

flag_tw

🇹🇼

flag_tz

🇹🇿

flag_ua

🇺🇦

flag_ug

🇺🇬

flag_um

🇺🇲

flag_us

🇺🇸

flag_uy

🇺🇾

flag_uz

🇺🇿

flag_va

🇻🇦

flag_vc

🇻🇨

flag_ve

🇻🇪

flag_vg

🇻🇬

flag_vi

🇻🇮

flag_vn

🇻🇳

flag_vu

🇻🇺

flag_wf

🇼🇫

flag_ws

🇼🇸

flag_xk

🇽🇰

flag_ye

🇾🇪

flag_yt

🇾🇹

flag_za

🇿🇦

flag_zm

🇿🇲

flag_zw

🇿🇼

fleur-de-lis

⚜

four

4️⃣

fr

🇫🇷

gb

🇬🇧

girl_tone1

👧🏻

girl_tone2

👧🏼

girl_tone3

👧🏽

girl_tone4

👧🏾

girl_tone5

👧🏿

guardsman_tone1

💂🏻

guardsman_tone2

💂🏼

guardsman_tone3

💂🏽

guardsman_tone4

💂🏾

guardsman_tone5

💂🏿

haircut_tone1

💇🏻

haircut_tone2

💇🏼

haircut_tone3

💇🏽

haircut_tone4

💇🏾

haircut_tone5

💇🏿

hand_splayed_tone1

🖐🏻

hand_splayed_tone2

🖐🏼

hand_splayed_tone3

🖐🏽

hand_splayed_tone4

🖐🏾

hand_splayed_tone5

🖐🏿

handball_tone1

🤾🏻

handball_tone2

🤾🏼

handball_tone3

🤾🏽

handball_tone4

🤾🏾

handball_tone5

🤾🏿

handshake_tone1

🤝🏻

handshake_tone2

🤝🏼

handshake_tone3

🤝🏽

handshake_tone4

🤝🏾

handshake_tone5

🤝🏿

hash

#️⃣

horse_racing_tone1

🏇🏻

horse_racing_tone2

🏇🏼

horse_racing_tone3

🏇🏽

horse_racing_tone4

🏇🏾

horse_racing_tone5

🏇🏿

information_desk_person_tone1

💁🏻

information_desk_person_tone2

💁🏼

information_desk_person_tone3

💁🏽

information_desk_person_tone4

💁🏾

information_desk_person_tone5

💁🏿

it

🇮🇹

jp

🇯🇵

juggling_tone1

🤹🏻

juggling_tone2

🤹🏼

juggling_tone3

🤹🏽

juggling_tone4

🤹🏾

juggling_tone5

🤹🏿

kiss_mm

👨❤💋👨

kiss_ww

👩❤💋👩

kr

🇰🇷

left_facing_fist_tone1

🤛🏻

left_facing_fist_tone2

🤛🏼

left_facing_fist_tone3

🤛🏽

left_facing_fist_tone4

🤛🏾

left_facing_fist_tone5

🤛🏿

lifter_tone1

🏋🏻

lifter_tone2

🏋🏼

lifter_tone3

🏋🏽

lifter_tone4

🏋🏾

lifter_tone5

🏋🏿

man_dancing_tone1

🕺🏻

man_dancing_tone2

🕺🏼

man_dancing_tone3

🕺🏽

man_dancing_tone4

🕺🏾

man_dancing_tone5

🕺🏿

man_in_tuxedo_tone1

🤵🏻

man_in_tuxedo_tone2

🤵🏼

man_in_tuxedo_tone3

🤵🏽

man_in_tuxedo_tone4

🤵🏾

man_in_tuxedo_tone5

🤵🏿

man_tone1

👨🏻

man_tone2

👨🏼

man_tone3

👨🏽

man_tone4

👨🏾

man_tone5

👨🏿

man_with_gua_pi_mao_tone1

👲🏻

man_with_gua_pi_mao_tone2

👲🏼

man_with_gua_pi_mao_tone3

👲🏽

man_with_gua_pi_mao_tone4

👲🏾

man_with_gua_pi_mao_tone5

👲🏿

man_with_turban_tone1

👳🏻

man_with_turban_tone2

👳🏼

man_with_turban_tone3

👳🏽

man_with_turban_tone4

👳🏾

man_with_turban_tone5

👳🏿

massage_tone1

💆🏻

massage_tone2

💆🏼

massage_tone3

💆🏽

massage_tone4

💆🏾

massage_tone5

💆🏿

metal_tone1

🤘🏻

metal_tone2

🤘🏼

metal_tone3

🤘🏽

metal_tone4

🤘🏾

metal_tone5

🤘🏿

mountain_bicyclist_tone1

🚵🏻

mountain_bicyclist_tone2

🚵🏼

mountain_bicyclist_tone3

🚵🏽

mountain_bicyclist_tone4

🚵🏾

mountain_bicyclist_tone5

🚵🏿

mrs_claus_tone1

🤶🏻

mrs_claus_tone2

🤶🏼

mrs_claus_tone3

🤶🏽

mrs_claus_tone4

🤶🏾

mrs_claus_tone5

🤶🏿

muscle_tone1

💪🏻

muscle_tone2

💪🏼

muscle_tone3

💪🏽

muscle_tone4

💪🏾

muscle_tone5

💪🏿

nail_care_tone1

💅🏻

nail_care_tone2

💅🏼

nail_care_tone3

💅🏽

nail_care_tone4

💅🏾

nail_care_tone5

💅🏿

nine

9️⃣

no_good_tone1

🙅🏻

no_good_tone2

🙅🏼

no_good_tone3

🙅🏽

no_good_tone4

🙅🏾

no_good_tone5

🙅🏿

non-potable-water

🚱

nose_tone1

👃🏻

nose_tone2

👃🏼

nose_tone3

👃🏽

nose_tone4

👃🏾

nose_tone5

👃🏿

ok_hand_tone1

👌🏻

ok_hand_tone2

👌🏼

ok_hand_tone3

👌🏽

ok_hand_tone4

👌🏾

ok_hand_tone5

👌🏿

ok_woman_tone1

🙆🏻

ok_woman_tone2

🙆🏼

ok_woman_tone3

🙆🏽

ok_woman_tone4

🙆🏾

ok_woman_tone5

🙆🏿

older_man_tone1

👴🏻

older_man_tone2

👴🏼

older_man_tone3

👴🏽

older_man_tone4

👴🏾

older_man_tone5

👴🏿

older_woman_tone1

👵🏻

older_woman_tone2

👵🏼

older_woman_tone3

👵🏽

older_woman_tone4

👵🏾

older_woman_tone5

👵🏿

one

1️⃣

open_hands_tone1

👐🏻

open_hands_tone2

👐🏼

open_hands_tone3

👐🏽

open_hands_tone4

👐🏾

open_hands_tone5

👐🏿

person_frowning_tone1

🙍🏻

person_frowning_tone2

🙍🏼

person_frowning_tone3

🙍🏽

person_frowning_tone4

🙍🏾

person_frowning_tone5

🙍🏿

person_with_blond_hair_tone1

👱🏻

person_with_blond_hair_tone2

👱🏼

person_with_blond_hair_tone3

👱🏽

person_with_blond_hair_tone4

👱🏾

person_with_blond_hair_tone5

👱🏿

person_with_pouting_face_tone1

🙎🏻

person_with_pouting_face_tone2

🙎🏼

person_with_pouting_face_tone3

🙎🏽

person_with_pouting_face_tone4

🙎🏾

person_with_pouting_face_tone5

🙎🏿

point_down_tone1

👇🏻

point_down_tone2

👇🏼

point_down_tone3

👇🏽

point_down_tone4

👇🏾

point_down_tone5

👇🏿

point_left_tone1

👈🏻

point_left_tone2

👈🏼

point_left_tone3

👈🏽

point_left_tone4

👈🏾

point_left_tone5

👈🏿

point_right_tone1

👉🏻

point_right_tone2

👉🏼

point_right_tone3

👉🏽

point_right_tone4

👉🏾

point_right_tone5

👉🏿

point_up_2_tone1

👆🏻

point_up_2_tone2

👆🏼

point_up_2_tone3

👆🏽

point_up_2_tone4

👆🏾

point_up_2_tone5

👆🏿

point_up_tone1

☝🏻

point_up_tone2

☝🏼

point_up_tone3

☝🏽

point_up_tone4

☝🏾

point_up_tone5

☝🏿

pray_tone1

🙏🏻

pray_tone2

🙏🏼

pray_tone3

🙏🏽

pray_tone4

🙏🏾

pray_tone5

🙏🏿

pregnant_woman_tone1

🤰🏻

pregnant_woman_tone2

🤰🏼

pregnant_woman_tone3

🤰🏽

pregnant_woman_tone4

🤰🏾

pregnant_woman_tone5

🤰🏿

prince_tone1

🤴🏻

prince_tone2

🤴🏼

prince_tone3

🤴🏽

prince_tone4

🤴🏾

prince_tone5

🤴🏿

princess_tone1

👸🏻

princess_tone2

👸🏼

princess_tone3

👸🏽

princess_tone4

👸🏾

princess_tone5

👸🏿

punch_tone1

👊🏻

punch_tone2

👊🏼

punch_tone3

👊🏽

punch_tone4

👊🏾

punch_tone5

👊🏿

rainbow_flag

🏳🌈

raised_back_of_hand_tone1

🤚🏻

raised_back_of_hand_tone2

🤚🏼

raised_back_of_hand_tone3

🤚🏽

raised_back_of_hand_tone4

🤚🏾

raised_back_of_hand_tone5

🤚🏿

raised_hand_tone1

✋🏻

raised_hand_tone2

✋🏼

raised_hand_tone3

✋🏽

raised_hand_tone4

✋🏾

raised_hand_tone5

✋🏿

raised_hands_tone1

🙌🏻

raised_hands_tone2

🙌🏼

raised_hands_tone3

🙌🏽

raised_hands_tone4

🙌🏾

raised_hands_tone5

🙌🏿

raising_hand_tone1

🙋🏻

raising_hand_tone2

🙋🏼

raising_hand_tone3

🙋🏽

raising_hand_tone4

🙋🏾

raising_hand_tone5

🙋🏿

right_facing_fist_tone1

🤜🏻

right_facing_fist_tone2

🤜🏼

right_facing_fist_tone3

🤜🏽

right_facing_fist_tone4

🤜🏾

right_facing_fist_tone5

🤜🏿

rowboat_tone1

🚣🏻

rowboat_tone2

🚣🏼

rowboat_tone3

🚣🏽

rowboat_tone4

🚣🏾

rowboat_tone5

🚣🏿

ru

🇷🇺

runner_tone1

🏃🏻

runner_tone2

🏃🏼

runner_tone3

🏃🏽

runner_tone4

🏃🏾

runner_tone5

🏃🏿

santa_tone1

🎅🏻

santa_tone2

🎅🏼

santa_tone3

🎅🏽

santa_tone4

🎅🏾

santa_tone5

🎅🏿

selfie_tone1

🤳🏻

selfie_tone2

🤳🏼

selfie_tone3

🤳🏽

selfie_tone4

🤳🏾

selfie_tone5

🤳🏿

seven

7️⃣

shrug_tone1

🤷🏻

shrug_tone2

🤷🏼

shrug_tone3

🤷🏽

shrug_tone4

🤷🏾

shrug_tone5

🤷🏿

six

6️⃣

spy_tone1

🕵🏻

spy_tone2

🕵🏼

spy_tone3

🕵🏽

spy_tone4

🕵🏾

spy_tone5

🕵🏿

surfer_tone1

🏄🏻

surfer_tone2

🏄🏼

surfer_tone3

🏄🏽

surfer_tone4

🏄🏾

surfer_tone5

🏄🏿

swimmer_tone1

🏊🏻

swimmer_tone2

🏊🏼

swimmer_tone3

🏊🏽

swimmer_tone4

🏊🏾

swimmer_tone5

🏊🏿

three

3️⃣

thumbsdown_tone1

👎🏻

thumbsdown_tone2

👎🏼

thumbsdown_tone3

👎🏽

thumbsdown_tone4

👎🏾

thumbsdown_tone5

👎🏿

thumbsup_tone1

👍🏻

thumbsup_tone2

👍🏼

thumbsup_tone3

👍🏽

thumbsup_tone4

👍🏾

thumbsup_tone5

👍🏿

two

2️⃣

uk

🇬🇧

us

🇺🇸

v_tone1

✌🏻

v_tone2

✌🏼

v_tone3

✌🏽

v_tone4

✌🏾

v_tone5

✌🏿

vulcan_tone1

🖖🏻

vulcan_tone2

🖖🏼

vulcan_tone3

🖖🏽

vulcan_tone4

🖖🏾

vulcan_tone5

🖖🏿

walking_tone1

🚶🏻

walking_tone2

🚶🏼

walking_tone3

🚶🏽

walking_tone4

🚶🏾

walking_tone5

🚶🏿

water_polo_tone1

🤽🏻

water_polo_tone2

🤽🏼

water_polo_tone3

🤽🏽

water_polo_tone4

🤽🏾

water_polo_tone5

🤽🏿

wave_tone1

👋🏻

wave_tone2

👋🏼

wave_tone3

👋🏽

wave_tone4

👋🏾

wave_tone5

👋🏿

woman_tone1

👩🏻

woman_tone2

👩🏼

woman_tone3

👩🏽

woman_tone4

👩🏾

woman_tone5

👩🏿

writing_hand_tone1

✍🏻

writing_hand_tone2

✍🏼

writing_hand_tone3

✍🏽

writing_hand_tone4

✍🏾

writing_hand_tone5

✍🏿

zero

0️⃣

Room Selector

The Room Selector is an element that allows users to find and select both rooms or persons. It behaves the same way as the chat selector you see when you forward a message and select where the message should be forwarded.

When a user starts typing in the field, a list of conversations and people will be displayed for selection. Only the chats that the user has access to will be displayed.

Attributes

Attribute

Type

Required?

Description

name

String

Yes

Identifies the room selector.

placeholder

String

No

Specifies a short hint that describes the expected value of the field.

required

Boolean

No

If true, at least one chat or person must be selected to be able to submit the form.

label

String

No

Label displayed on top of the Room Selector.

value

Array of

string

No

Default values that will be preselected in the Room Selector. The array can contain both streamIds, as well as userIds. Please note that if the user does not have access to these users or conversations, they will not be displayed.

Rules and Limitations

  • The Room Selector element supports multi selection which means that you can search for more than one chat or person.

  • The Room Selector is not yet available on Symphony Mobile. Mobile will be supported in a future release.

Example

<messageML>
     <form id="test"> 
        <room-selector name="room-selector" 
            label="Select the chats where the alert will be sent." 
            value="['rsxB51ieSYfPLQ0jgFKg93___nUqhvz4dA','9139691037944']"  
            placeholder="Select rooms"/>
        <button name="submit_button" type="action">Create Alert</button>    
    </form>
</messageML>

Resulting payload when a user submitted the form afer having selected two chats and one user in the Room Selector.

"payload": {
    "symphonyElementsAction": {
        "stream": {
            "streamId": "9vwxSTElsJDEqmRt2CWQpn___nRRfCVgdA",
            "streamType": "IM"
        },
        "formMessageId": "t2AFSQooaBvh_OrDrNDR53___nGF6NqybQ",
        "formId": "test",
        "formValues": {
            "action": "submit_button",
            "room-selector": {
                "userIds": [
                    "9139691037944"
                ],
                "streamIds": [
                    "fy0RUFUt6pkiiV3nPZVr5X///nJeKqjMdA==",
                    "5fXylrRrqEb1vH4bvBMWNn///nWQegjtdA=="
                ]
            }
        }
    }
}

Versions and Compatibility

Main features introduced

Agent needed to parse message sent by the bot

Client 2.0 release

Backward client-compatibility behavior (e.g. external rooms)

Feature introduced

23.11

C2 24.4 (April 2024)

Not available yet. Room Selector is not displayed.

Time Picker

Do you need users to pick a time as part of an interactive flow? With the Time Picker element, Symphony users can easily select a time from a list of possible values.

The Time Picker offers to Symphony users the possibility to enter the time in two different ways:

  • Populating the field directly with the keyboard, following the appropriate format and the list of possible values (see attributes strict, min, max, and disabled-time)

  • Clicking on the wished time from the dropdown displayed

MessageML tag

The Time Picker is represented by the <time-picker> tag, as you can see in the examples at the bottom of the page.

Designs

You can see below the designs of the time picker.

Attributes

Attribute

Type

Required?

Description

name

String

Yes

Identifies the time picker.

value

String "HH:mm:ss"

No

Time with ISO_8601 format to be displayed by default by the time picker when rendered for the Symphony user. Overrides the placeholder value.

title

String (accepting \n for line break)

No

Description that will be displayed when clicking the tooltip icon located on top of the time picker Element.

label

String

Not required but it is recommended if title is defined

The text of the label that will be displayed above the time picker field.

required

Boolean (true or false)

No

If true, it specifies that a time must be picked before submitting the form.

placeholder

String

No

Specifies a short hint that describes the expected format of the time picker field. If the attributevalue is entered, the placeholder will not be displayed. If the placeholder is not set, the accepted time format will be displayed by default. Note: We recommend to use the default placeholder. It is better to rely on the label if context is needed, or the title if instructions are needed.

min

String "HH:mm:ss"

No

Specifies the earliest acceptable time with ISO_8601 format.

Note: Times earlier than the min are not displayed in the time picker.

max

String "HH:mm:ss"

No

Specifies the latest acceptable time with ISO_8601 format.

Note: Times later than the max are not displayed in the time picker.

disabled-time

Json array (in a String format)

No

Times or ranges of times that cannot be selected by the Symphony user. Maximum length of 1024 characters. There are 2 different patterns: 1. range of times: {"from": "HH:mm:ss", "to": "HH:mm:ss"}, 2. specific time: {"time": "HH:mm:ss"}. Important: single quote ' should wrap the xml parameter. See the examples below for more details.

Note: Disabled times are displayed as disabled values in the time picker.

format

String

No

Format in which the time will be displayed to or should be entered by the Symphony user. Only accepts the letters 'h', 'H', 'm', 's', and 'a' as well as ':' or space as separator. • 'h' (for 12-hour format) and 'H' (for 24-hour format) define the hour. You can use either 1 ('h'; 'H') or 2 ('hh'; 'HH') to define the minimum number of digits displayed (i.e. corresponding to "3" or "03" for the third hour) • 'm' defines the minutes. Similarly to the hours, you can use one or two ('m' or 'mm') • 's' defines the seconds. Similarly to the hours and minutes, you can use one or two ('s' or 'ss')

• 'a' (usually placed after a space) allows to display 'AM' for morning times and 'PM' for afternoon times Note 1: We recommend the use of the default value as much as possible. Note 2: The format only impacts what the end user will see. It does not affect how you have to specify the value, min, max, disabled-time, or the format of the user reply.

step

Number

No

Default is 900 (corresponding to15 min)

The stepping interval (in seconds) for the times displayed in the dropdown menu.

Min value: 600 (corresponding to 10 min)

Max value: 43 200 (corresponding to half a day)

strict

Boolean

No

Default is false

Enforce that the user cannot select a time that is not in the proposed list (e.g. we want him to only select an hour, he can’t decide to set the field to 9:15, even with the keyboard).

Please note that in addition to this, the value picked will be validated against attributes min, max, and disabled-time.

Accessibility

For the purpose of accessibility, Symphony users can interact with the time picker via their keyboard:

  • First, using "Tab" to enter the component

  • Using "Enter" or "Space" to open the dropdown when focus is on the icon, or just "Enter" when focus is on the input

  • Using "Arrow up" or "Arrow down" to navigate within the dropdown list and using "Enter" to select the preselected value

  • Either "Typing" or pressing "Tab" to go from the hours to the minutes, and then from the minutes to the seconds

  • Either "Deleting" or pressing "Shift+Tab" to go back from the seconds to the minutes, and then from the minutes to the hours

  • Finally using "Tab" to exit the component

Rules and Limitations

  • The max length of any time picker attribute is 256 except disabled-time attribute which max length is set to 1024 characters.

  • If the format entered by the user is not correct or if the time entered is a disabled time, then an error message is displayed to the user. Note that it is not possible for the user to submit the form with an invalid format or disabled time.

  • You can add a default time in your text field by including it in the value parameter. Please note that unlike the placeholder text, the default time will be sent back to the Bot in the user reply when the form is submitted if it is not edited by the user.

  • The time-picker will be supported on the following versions of clients:

    • 20.14 for Client 2.0

    • 20.13 for Client 1.5

Please note that a limited amount of values are displayed in the dropdown of the time picker (see attribute step which minimum is 10 min). However, you can note that the time picker supports a precision to the second (see format attribute). Also, if you don't use the attribute strict, then user will be able to populate the field via their keyboard with any other non-disabled and existing time value than proposed in the list.

Examples

The following examples show the time picker being used as follows:

  • The first time-picker (init) shows how to display a default time, as an initial value is entered as parameter. Note that the default value is present in the user reply as it has not been deleted before submitting the form.

  • The second time-picker (placeholder) shows how a placeholder is displayed in the UI. Please note that any text is accepted as input. However, if you compare with the next time-pickers present in the form, you can notice that a default placeholder is generated (with a hint of the correct format to accepted by the time-picker field) in case no placeholder is set.

  • The third time-picker (label) shows how a label ("My Label") is displayed.

  • The fourth time-picker (tooltip) shows how the title attribute ("My Tooltip/n With a second line") is displayed.

  • The fifth time-picker (noreq) shows how a user can interact with a non-required field. Even if the field is empty (only a placeholder text is present but does not count as a value), it does not prevent the user from submitting the form.

  • The sixth time-picker (req) shows the behaviour of the unique required field of the form, which cannot be submitted in case it is not filled.

  • The seventh time-picker (format) shows the way the placeholder evolves to adapt to a new format transmitted thanks to the format parameter. Also please note the accessible interaction with the time-picker via the keyboard.

  • The eighth time-picker (step) shows how to modify the step between 2 consecutive values in the list of times. Please note that a Symphony user can select any other existing time by populating the field with his keyboard.

  • The ninth time-picker (strict_step) shows how to enforce the user to choose among the non-disabled values displayed in the dropdown, thanks to the strict attribute.

  • The tenth time-picker (rules) shows how to interact with the following parameters: min, max, and disabled-time. Please note that a disabled time cannot be entered manually.

<messageML>
  <form id="form_id">
    <h2>time-pickers</h2>
      <time-picker name="init" label="With default value" value="13:51:06" />
      <time-picker name="placeholder" placeholder="Only Placeholder..." />
      <time-picker name="label" label="My Label" />
      <time-picker name="tooltip" label="With Tooltip" title="My Tooltip\n With a second line" />
      <time-picker name="noreq" placeholder="Not required" />
      <time-picker name="req" required="true" placeholder="Required" />
      <time-picker name="format" label="Other Format" format="hh:mm a" />
      <time-picker name="step" label="With a customed time-step" step="600" />
      <time-picker name="strict_step" label="Restricting values to the time-step defined" strict="true" />
      <time-picker name="rules" label="With Disabled values" min="08:00:00" max="17:00:00" disabled-time='[{"from": "12:00:00", "to": "14:00:00"}, {"time": "15:00:00"}]' />
      <button name="time-picker">Submit</button>
  </form>
</messageML>
[
    {
        "id": "uptHwx",
        "messageId": "9msDL6zbxKpXA82fG86tYH___oe6lh_LbQ",
        "timestamp": 1616072269876,
        "type": "SYMPHONYELEMENTSACTION",
        "initiator": {
            "user": {
                "userId": 7078106482890,
                "firstName": "User",
                "lastName": "Bot",
                "displayName": "User",
                "email": "userbot@symphony.com",
                "username": "user_bot"
            }
        },
        "payload": {
            "symphonyElementsAction": {
                "stream": {
                    "streamId": "tp3eVYR3ZTyW7K7vmsmgYX___oq6-yN5dA",
                    "streamType": "ROOM"
                },
                "formMessageId": "UG1jcvtbGMdRlWQlq1AXgH___oe65VckbQ",
                "formId": "form_id",
                "formValues": {
                    "action": "time-picker",
                    "init": "13:51:06",
                    "placeholder": "01:30:00",
                    "label": "",
                    "tooltip": "",
                    "noreq": "",
                    "req": "12:45:02",
                    "format": "00:45:00",
                    "step": "00:10:02",
                    "strict_step": "00:15:00",
                    "rules": "14:15:00"
                }
            }
        }
    }
]

Versions and Compatibility

Main features introduced

Agent needed to parse message sent by the bot

Client 2.0 release

Backward client-compatibility behavior (e.g. external rooms)

Initial release

20.12

20.14

Not working

Standard Entities

This section lists the Structured Objects available for use in messages.

Standard Entities

Article

Field

Required

Format

Description

title

Yes

String

The headline of the article

subTitle

No

String

The subtitle of the article

blurb

No

String

A summary of the article to display

date

No

Date of publication

publisher

No

String

Name of the publisher

author

No

String

Name of the author

thumbnail

No

URL (could be a data url)

Image to be displayed - 106x106px

id

Must provide either id or href, or both.

String

An identifier used by the application to deeplink to the article

href

Must provide either id or href, or both.

URL

URL to the article (opened in a new browser window)

Financial Objects

org.symphonyoss.fin.security

Field

Required

Format

Description

type

Yes

String

The type of object. Must be set to org.symphonyoss.fin.security.

id

Yes

Array of Objects

An array of one or more of the following objects: org.symphonyoss.fin.security.id.ticker org.symphonyoss.fin.security.id.isin org.symphonyoss.fin.security.id.cusip org.symphonyoss.fin.security.id.openfigi More information about these objects is provided below.

org.symphonyoss.fin.security.id.ticker

Field

Required

Format

Description

type

Yes

String

The type of object. Must be set to org.symphonyoss.fin.security.id.ticker.

value

Yes

String

The name/ID of a ticker.

org.symphonyoss.fin.security.id.isin

Field

Required

Format

Description

type

Yes

String

The type of object. Must be set to org.symphonyoss.fin.security.id.isin.

value

Yes

String

The entity's ID.

org.symphonyoss.fin.security.id.cusip

Field

Required

Format

Description

type

Yes

String

The type of object. Must be set to org.symphonyoss.fin.security.id.cusip.

value

Yes

String

The entity's ID.

org.symphonyoss.fin.security.id.openfigi

Field

Required

Format

Description

type

Yes

String

The type of object. Must be set to org.symphonyoss.fin.security.id.openfigi.

value

Yes

String

The entity's ID.

FDC3 Action buttons

FDC3 action buttons allow chat bots to embed buttons in messages which, on click, will raise an intent.

Action buttons are only displayed to users who have a Desktop Integration Platform (DIP) set up, such as interop.io, Here Core or Connectifi.

MessageML (message property)

<messageML> This is a FDC3 action button: 
    <br/>
    <span class="entity" data-entity-id="0">View Chart 
        <span class="tempo-text-color--red" style="font-size: 10px">
            <i>(Action button: Desktop Integration Platform is not available.) </i>
        </span>
    </span>
</messageML>

Note: Please see above the default text that would be displayed to users who don't have a desktop integration platform set up.

EntityJSON part (data property)

{
"0":
    {
    "actionData":
        {
        "context":
            {
            "id":
                {
                "ticker": "TSLA"
                },
            "name": "Tesla, inc.",
            "type": "fdc3.instrument"
            },
        "intent": "ViewChart"
        },
    "label": "View a chart",
    "type": "fdc3.entity.action",
    "version": "1.2"
    }
}

Display in Symphony:

Image

Field

Required

Format

Description

type

Yes

String

The type of entity. Must be set to com.symphony.media.image.

version

Yes

String

The version.

format

Yes

String

The data format. Must be set to image.

url

Yes

String

The URL of the image.

Taxonomy (mention)

Field

Required

Format

Description

type

Yes

String

The type of object. Must be set to com.symphony.user.mention.

version

Yes

String

The object's version.

id

Yes

Array of objects

An array of one or more of the following objects: • com.symphony.user.userId More information about these objects is provided below.

com.symphony.user.userId

Field

Required

Format

Description

type

Yes

String

The type of object. Must be set to com.symphony.user.userId.

value

Yes

String

The ID of a user.

Video

Field

Required

Format

Description

type

Yes

String

The type of object. Must be set to com.symphony.media.video.

version

Yes

String

The version.

format

Yes

String

The video's format. Must be set to youtube or vimeo.

url

Yes

String

The URL of the video.

id

Yes

String

The unique ID of the video (can be extracted from the video URL).

Go further...

Continue here to learn more about structured objects:

Entities

Entities (also called Structured Objects) are rich, inline, interactive components for Symphony messages that allow you to embed information that is more complex than simple text.

  • class="entity": specifies that the message contains a corresponding structured object.

  • data-entity-id: the unique ID of the corresponding structure object.

This MessageML markup is then passed into the endpoint via the message parameter. The following examples show the use of the attributes in <div> and <span> respectively:

<div class="entity" data-entity-id="entityIdentifier">An object</div>
<span class="entity" data-entity-id="entityIdentifier">An inline object</span>

The examples above reference an entity object called entityIdentifier. The JSON corresponding to this object is passed to the create message endpoint via the data parameter. For example:

Data:
{
  "entityIdentifier": {
    "type": "org.symphonyoss.fin.security",
    "version": "0.1",
    "id": [{
      "type": "org.symphonyoss.fin.security.id.isin",
      "value": "US0378"
    },
      {
        "type": "org.symphonyoss.fin.security.id.cusip",
        "value": "037"
      },
      {
        "type": "org.symphonyoss.fin.security.id.openfigi",
        "value": "BBG000"
      }
    ]
  }
}

Please continue below in the subpages if you want to learn more about Structured Objects

PresentationML

Messages in PresentationML markup are enclosed in a <div> tag with the following attributes:

  • data-format: must be set to PresentationML.

  • data-version: specifies the markup version.

<div data-format=\"PresentationML\" data-version=\"2.0\"> content here... </div>

Shorthand tags translations:

The following table lists XHTML tags for MessageML on the left, and the corresponding PresentationML tags on the right:

Shorthand tag in MessageML

PresentationML Translation

<messageML>

<div data-format=\"PresentationML\" data-version=\"2.0\">

<mention uid="123456789"/>

<span class="entity" data-entity-id="mention123">@Name</span>

<hash tag="hashtag"/>

<span class="entity" data-entity-id="keyword123">#hashtag</span>

<cash tag="ticker"/>

<span class="entity" data-entity-id="keyword456">$tag</span>

<chime />

<audio src="https://asset.symphony.com/symphony/audio/chime.mp3" autoplay="true" />

<card>

<div class="card barStyle" data-icon-src="url" data-accent-color="blue"> <div class="cardHeader">PresentationML</div> <div class="cardBody">PresentationML</div> </div>

Root <div> tag When retrieving a message using the API, the message is always encapsulated in a root <div> tag, for easy parsing.

When you create a message using PresentationML, you must include the root <div> tag.

Tag

Description

Attributes

root <div>

The root element of a Symphony message, when read through the API.

• data-format: must be set to "PresentationML" • data-version

The following is an example of content for a simple message using presentationML markup:

<div data-format=\"PresentationML\" data-version=\"2.0\">
  This is a message.
</div>

Custom Entities

Definition

Structured Objects are rich, inline, interactive components of Symphony messages. Objects allow you to build innovative workflows that go beyond working with normal text or attached files.

  • Unlike normal message text, these objects are structured and do not need to be parsed to have business logic.

  • Unlike attachments, end-users can view and interact with objects directly from their Symphony client, without having to change context.

  • Structured Objects can be "injected" into Symphony by sending messages using Symphony's REST API.

Prerequisites

To inject messages containing structured objects:

  • Your pod must be configured for Symphony's REST API, and you must have the Agent, the component used for handling encryption and decryption of messages and content, set up.

  • Your Agent must be version 1.51 or later.

  • You must have an X.509 identity certificate for your bot service user for REST API authentication, where the common name on the certificate matches your service user's username.

To build renderer applications for displaying your structured object:

  • You need to have an extension application created and enabled on your pod.

Sending Structured Objects in Messages

Structured Objects are placed into Symphony messages and have two components:

  • Object Data, a JSON object.

Any message in Symphony can contain zero or more Structured Objects.

  • A message will always contain message presentation, in MessageML v2 format, with the optional object presentation of the Structured Objects it may contain.

  • If the message contains any structured objects, it will contain JSON data with all object data of the structured objects it may contain.

  • The message parameter, which contains the message presentation, with the object presentation for each Structured Object.

  • The data parameter, which contains JSON data with the object presentation for each structured object.

Message and Object Presentation

Message presentation is represented in MessageML format. For example:HTML

<messageML>
  Hello <mention email="user@music.org" />. Here is an important message
</messageML>

To add an object to a message, include a div or a span tag with a unique data-entity-id attribute:HTML

<messageML>
  Hello <mention email="user@music.org" />. Here is an important message with an
  <div class="entity" data-entity-id="object001" /> 
  included.
</messageML>

The data-entity-id tag refers to a specific object in the JSON data, which needs to include:

  • The data type.

  • The data version of that type. Both are needed to build renderer applications which can render this type of that version.

{
    "object001":
    {
        "type":     "org.symphonyoss.fin.security",
        "version":  "1.0",
        "id":
        [
            {
                "type":     "org.symphonyoss.fin.security.id.ticker",
                "value":    "IBM"
            },
            {
                "type":     "org.symphonyoss.fin.security.id.isin",
                "value":    "US0378331005"
            },
            {
                "type":     "org.symphonyoss.fin.security.id.cusip",
                "value":    "037833100"
            }
        ]
    }
}

This data can be used by applications in the web client to provide a rich display or end-user interactivity. In case no specific renderer application is available, you must provide a default presentation in the div or a span tags.HTML

<messageML>
  Hello <mention email="user@music.org" />. Here is an important message with an 
  <div class="entity" data-entity-id="object001">object</div> 
  included.
</messageML>

Reading objects

Note:

Renderer Applications

Renderer Applications leverage the Extension API to dynamically replace the presentation of a structured object. To create a renderer application:

Go further...

To learn more about building Extension Applications that leverage structured objects, continue here:

Regular Expressions - Regex

Elements input validation. Available from Symphony v20.6 and above

Input validation is your first line of defense when creating a secure application.

Symphony Elements supports input validation using regular expressions (regex) for the text field and text area elements.

The regular expression pattern is validated on the front end by the Client. It is therefore critical to also validate the input in your code as well.

Regular expression Denial of Service - ReDoS

To prevent performance issues on the Client, a validation mechanism checks regular expressions to ensure they are safe. If the regular expression is unsafe, it is automatically disabled. In that situation, the user input will not be validated.

Validation examples

The following code snippets assumes that both <text-field> and <textarea> elements are placed within the right messageML context : <messageML> + <form> including an action <button>.

Note that the content of the regex has to be checked again on the bot side.

Text field

Text area

Dialog

Chat bots can embed dialog buttons in chat messages. When the dialog button is clicked, a dialog opens and displays the content that has been predefined. This can be useful to make long messages much more concise, or progressively disclose a form only if it is relevant to the user for example.

MessageML tags

The Dialog component is structured in two sections:

  • <dialog> tag that defines the content of the dialog that will be displayed when the button is clicked.

UI-action with open-dialog

The <ui-action> tag wraps a single <button> children tag. The button tag supports the usual styles (primary, secondary, tertiary). Examples are available at the bottom of this page.

The <ui-action> tag supports the following attributes:

Dialog tag

Each <dialog> is split in three areas (children tags):

  • <title> - mandatory: specifies the title of the dialog and is always displayed at the top of it in a fixed and non scrollable position.

  • <body> - mandatory: specifies the content of the dialog that is displayed in the middle of it and can be scrollable when the content is too big to be contained in the view height.

  • <footer> - optional: specifies the footer of the dialog and is always displayed at the bottom in a fixed and non scrollable position.

The <dialog> tag supports the following attributes

Examples

The following gif shows an example that contains two dialogs:

  1. The first dialog is embedded in a form. You will notice that the button used to trigger the dialog is positioned where the <ui-action> tag is coded.

  • As soon as the associated <dialog> is at its same nesting level, it can be placed anywhere before or after the ui-action.

  • You can also notice that users can interact with the form as well as submit it.

  • However, the dialog only contains text as it cannot contain any interactive Form Elements. Please note that a cancel button can be included in the dialog as it is not considered as an interactive Form Element: you can use it to offer another option for the user to close the dialog.

  • Please note the scrolling behaviour of the body whereas both title and footer are placed in a fixed position.

  1. The second dialog embeds a form instead.

  • After having started to fill-in the form, if the user closes the dialog and opens it back, the values of the fields will not reset to their original values, as long as the user does not refresh the page.

  • Also, when submitting a form that is in a dialog, the dialog will automatically close after a certain delay. If the user opens back the dialog without refreshing the page, then the values of the form and its state are persisted.

Simpler examples

Below are simpler examples with and without the use of forms.

Rules and Limitations

  • The max length of any ui-action or dialog attribute is 256.

  • Please note that the attribute type of the button is not supported when wrapped by a <ui-action> tag.

  • The dialog and ui-action tags must be both present in the same message, at the same nesting level, and they must share the same and unique id / target-id. Please note it is possible to have several dialogs in the same message as soon as each dialog has a different id.

  • The dialog feature is supported in popped-out mode. The dialog will open in the popped-out window.

    • Dialogs can be contained inside forms. However, if contained in a form, the dialog cannot contain any interactive Element (such as button, text-area, etc.)

    • Dialogs can contain a form. The <form> tag should wrap the entire content of the dialog, including the <title> ,<body> and <footer> tags, as you can see in the examples below. This is useful when you want the submit button not to be hidden and always appear in the footer of the dialog whereas the rest of the form content is contained in the scrollable body area.

  • A dialog cannot be embedded in another dialog.

Best Practice for using dialogs

Even if this possible, please avoid embedding more than one form in a single dialog.

Versions and Compatibility

Bots Best Practices

Symphony has a number of best practices to ensure smooth bot development and a positive user experience.

Getting Your Bots Attention

It is common that Symphony chatrooms contain more than one bot listening for events. In order to preserve name space and ensure that there isn't overlap between bot workflows, we recommend that bots only respond to events that begin with an @mention:

By '@mentioning' your bot, it ensures that only the intended bot responds.

Commands

It is best practice to have bots listen for commands beginning with a '/'. This naming convention makes it clear to other users that you are trying to get a bot's attention. Additionally, it makes it clear to your bot that a user is instructing it to perform an actions and kick off its intended workflow.

It is also best practice to enforce command line style arguments for commands that require additional input or data:

Help Menu

It important that users understand exactly what your bot is capable of and what commands your bot understands. It is best practice for your bot to list its commands inside a help menu:

Datafeed

It is considered best practice that bot's only create and read from one datafeed. If your bot goes down, it should re-authenticate, and try to read from the previously created datafeed. If this fails then you should create a new datafeed, and begin reading from this new datafeed.

Creating and reading from multiple datafeeds concurrently can result in your bot processing duplicate messages and subsequently sending duplicate or out of order messages back to the user.

All datafeed management and best practices are provided out of the box by our dedicated BDKs (Bot Developer Kit) and WDK toolkits.

Message Rate

For large rooms, it is recommended that bots do not send messages at a higher rate than 2 messages/sec or every 500ms.

Bulk Adding Users

If bots are bulk adding users to rooms, it is recommended that bots add users at rate of 3 users / second.

Duplicate Messages

In some rare cases, bots may receive duplicate messages from Symphony. In order to prevent duplicate processing, developers can implement logic to keep track of previous messages. It is recommended that bots store a list of unique messageIDs up to 15 minutes in the past. Upon each new message, bots should do a quick validation that new message is not received in the past and continue to process the message.

Logging

Open Source Code Samples

Many members of our community are building bots and code samples and publishing their source code. We collect these examples in this page. You can use any of them as is, or to either bootstrap your own idea or just for inspiration.

This list is provided for information only, without warranty of any kind. All source code are protected by their respective license.

Symphony Exporter

This bot provides users to retrieve and export all of their messages from Rooms and IMs.

Symphony Roomzilla

This bot provides capabilities to manage room creation and population using Active Directory, .csv files or .eml files (email).

Symphony TradeBuddy

This bot tracks watchlists for multiple users and displays a table with clickable buttons to allow users to join discussion rooms.

Symphony Poll Bot

This bot uses Symphony Elements to facilitate the creation of polls, firing of poll messages, ending polls and collation of results.

Building Extension Apps

Building Extension Apps on Symphony Messaging is an easy and secure way to customize the Symphony Messaging experience. Take these simple steps in order to create and deploy your Extension App today!

1. Plan Your App

Symphony Messaging Extension Apps leverage the Symphony Messaging Extension API in order to create innovative workflows and automations. Depending on your desired workflow, there are many different development avenues available for you and your development team. Understanding these different development options, APIs available and Extension App capabilities is key to creating a successful app and positive user experience. Learn more about the different types of Extension Apps and APIs here:

2. Leverage Our Tools

The easiest way to get started is by using the Symphony Messaging Generator to create a project that includes the App Development Kit (ADK) and the UI Toolkit. These tools allow you to interface with the Extension API and build interfaces that feel native in Symphony Messaging.

3. Add Custom Business Logic

The next step is to add custom business logic to your app. Begin leveraging Symphony Messaging's Extension API and bring your extension apps to life:

Getting Started with ADK

The App Developer Kit (ADK) is the preferred tooling for web developers to get started building extension apps on Symphony

Generate your Extension App

First, install yeoman and the Symphony Generator.

Then, create a directory for your new app project and launch the generator.

This will prompt you with a number of questions about your app and pod configuration. Type in your app ID, using arrow keys to scroll and press enter to move on to the next prompt.

When prompted for Select your type of application, choose Extension App (ADK). You then have a choice of different project types:

  • Basic - simple project to demonstrate initialization

  • App View - uses React with either JavaScript or TypeScript to create app views

  • Message Renderer - overrides rendering of messages with custom formatting

Create Application

When you are ready to deploy your app permanently (or if you require Circle of Trust), follow the instructions on this page to setup your app.

Start your App

Test your App

  1. Acknowledge the developer mode notice. Your app is now loaded.

Next Steps

Each project type in the generator corresponds to one of the guides below. Read the respective guide for explanations of how to use ADK.

Planning Your App

What is a Symphony Extension App?

Symphony extension apps are standalone web applications that are embedded within the Symphony user interface as iframes. These iframes interact with the Symphony container using the Symphony Extension API. Extension apps can be accessed by the end user in two ways:

  1. From the left navigation under the Applications tab. Clicking on your app name brings it into view

  2. Attached to contexts such as #hashtags, $cashtags, or user profiles. Clicking on your app button brings it into view along with the corresponding context.

Developing extension apps enable developers to enrich the Symphony experience and create custom workflows and automations on top of the Symphony platform.

How do Extension Apps create workflows and automations?

The answer lies in Symphony's Extension API, which is a JavaScript library that consists of services and methods that allow applications to extend and interact with Symphony's user interface. By leveraging these services, developers can:

  • Add modules, or windows bringing your app's content into the Symphony canvas

  • Add entry points for your app, such as navigation items on the left hand nav or #hashtags and $cashtags.

  • Add interactive buttons to chat and user profile module headers

  • Enable users to share content from your app into Symphony chats

For a full overview of Symphony's Extension API continue here:

Next Steps

Before you begin your extension app development journey, it is important to consider the following when determining what type of app you build:

1. What are the goals of your Symphony Extension App?

Before building your extension app, it's important that you identify the use cases that this app will serve. In other words, identify the ways in which this app will increase productivity, add meaningful color to your daily tasks, centralize information, reduce business pain points, and make working simpler for end users. To easily identify valuable use cases, ask yourself the following:

  • Are there numerous sources of information that I check daily that can be centralized inside of Symphony?

  • Are there any views or visualizations from third party apps which could be embedded into Symphony's UI?

  • Are there any tasks in my daily workflow that require manually sifting through large amounts of data?

  • Would it be helpful if this data could be accessed easily to Symphony users across my organization?

  • Are there any customized features on Symphony's UI that would increase my productivity and efficiency?

2. Who is your App's target audience?

The type of extension app you build will depend on who is using and interacting with it. To identify your app's audience, ask yourself the following:

  • Are the users of my app internal or external counter-parties?

  • Are the users of my app front-office or back-office employees mostly?

  • Will my app be interacting with a technical audience or a business audience?

  • What languages does your audience speak?

The more you understand your app's audience, the more you can understand their business pain points and in turn develop a better user-experience and app solution.

3. What sort of interactions will your App have?

Users can interact with extension applications in a number of different ways. Specifically, users can launch a standalone extension app from the left-hand nav, invoke the app by clicking on custom UI buttons on a users profile, launch the app by clicking on attached contexts such as #hashtags or $cashtags, and even allow authenticated apps to make actions on-behalf-of an authorized user. Before building your extension app, its important to identify the types of interactions between users and your app:

Will your application need to receive conversation or user data?

Many extension applications built on top of Symphony need to receive conversation or user data. For example, if you wanted to build an extension app that extends the Symphony UI to add buttons to the IM, profile, or chatroom modules, it is likely that you would need access to conversation or user data. In order to do so your application will need to perform app authentication. You can learn more about performing app authentication here:

Will your application customize modules by adding buttons to IMs, chatrooms, or user profiles?

Another common use case for extension applications is to extend various parts of the Symphony UI by adding buttons to IMs, chatrooms, or profile modules. In order to receive the conversation and user data associated with these modules, these extension apps must also perform app authentication. You can learn more about how to receive user and conversation data as well as adding buttons to Symphony modules here:

Will your application customize links added to the #hashtag and $cashtag hovercards?

Another way extension applications can extend Symphony's UI is to override links associated with #hashtag (e.g. #symphony) and $cashtag (e.g. $GOOG) hover cards. By attaching your extension app to #hashtag or $cashtag contexts, you can show content in your app that is relevant to the context clicked by the user. You can learn more about how build extension apps that extend these #hashtag and $cashtag entities in the same guide above.

Will your application perform custom rendering?

Extension apps can extend Symphony's UI by acting as a custom renderer for structured objects created by the REST API. Structured objects are rich, inline, and interactive components embedded in a Symphony message. These structured objects can be rendered and injected into Symphony by creating a custom renderer as a part of an extension application. You can learn more about how to create a custom renderer in order to render structured objects here:

Will your application contain a standalone frontend?

Some extension apps contain a dedicated frontend that will be embedded within of Symphony. For example, it's possible that your extension app will need to present complex financial data, charts, or dashboards, to an end user. Obviously, this functionality is not provided by Symphony out of the box, but you can bring it into Symphony through an extension application. Symphony provides a UI toolkit containing a library of react components in order to help build complex frontend applications rapidly. This UI toolkit contains frontend components and styles to allow you get the best out of your workflow.

To learn more about leveraging the UI Toolkit to build complex frontend applications continue here:

OpenChat

Chat bots can add buttons in chat messages that on click will open an existing chat room or start a chat with a specific user or group of users.

This can be useful to help Symphony users easily navigate from one chat to another, or to reference another discussion happening in a different chat.

The new chat can either replace the current chat, or open on the side instead.

MessageML tag

OpenChat is represented in MessageML as a <ui-action action='open-im'> tag and wraps a single button.

The <ui-action> tag for OpenChat supports the following attributes:

Accessibility

Symphony users can interact with the OpenChat functionality via their keyboard using either "Enter" of "Space" to trigger the click action, once the UI component used for the OpenChat (i.e. button element) is in focus. Symphony users can move from one Element to another using "Tab".

Examples

The following examples show the OpenChat functionality being used as follows:

  • The first open-im shows how the Symphony user may interact with a primary button that is empowered with the OpenChat functionality that opens a specific stream on the side of the current chat. Note that the trigger and side-by-side attributes are not mentioned, as they are not mandatory: they are then defined by their default behaviour.

  • The second open-im shows how the Symphony user may interact with a secondary button that is empowered with the OpenChat functionality that opens an IM with a specific user on the side of the current chat.

  • The third open-im shows how the Symphony user may interact with a tertiary button that is empowered with the OpenChat functionality that opens a chat with a specific list of users on the side of the current chat.

  • The fourth open-im shows how the Symphony user may interact with a destructive button that is empowered with the OpenChat functionality that opens a specific stream that replaces the current chat.

Rules and Limitations

  • The max length of any ui-action attribute is 256 except user-ids attribute which max length is set to a list 15 ids.

  • Please note that the attribute type of the button is not supported when wrapped by a <ui-action> tag.

  • When using user-ids attribute with 2 or more ids in the list, the functionality will always create a new chat between the considered users; it will not reopen an existing chat between these same users. This is a known limitation that might change in the future.

  • In case the side-by-side attribute is set to true, when the Symphony user interacts with the OpenChat component, the IM/Chatroom appears on the side of current module in focus. In case the current module is not part of a workspace, then clicking on the button automatically creates workspace and focuses automatically to the desired IM/chat.

  • The OpenChat feature is supported in popped-out mode with the following behaviour:

    • Current chat/IM on focus is popped-out independently from a workspace:

      • In case the side-by-side attribute is set to false, the popped-out module is replaced by the desired one

      • In case the side-by-side attribute is set to true, the popped-out module remains in place and the desired one opens in the main Symphony client

    • Current chat/IM on focus is part of a popped-out workspace (Capital Markets view only): same behaviour as if the workspace was not popped out: if the specific chat to be opened is not part of the workspace, it is automatically added, and then opened either on the side or in the place of the previous focused chat depending on the side-by-side attribute.

  • When opening a specific chatroom where the user is not part of, then:

    • If it is a private room, a modal dialog will appear informing the user he cannot perform the action

    • If it is a public room, then the user is automatically added to the room and the considered stream is opened

  • The side-by-side attribute set to false is meaningful only for users in Client 2.0

Versions and Compatibility

The Table Select is not an Element itself but an example of what can be achieved by using Elements with the templates. This way, it is possible to build tables which contain a special column that allows users to select one or more rows, either with the or the Element.

The following example shows how to create a Table Select structure using the template and a JSON file.

The typeattribute determines if a table will display a special column with or within it. Note that a table can have only one of the two possible types, being button or checkbox. For more information, see the Example below.

Note that the template is being used to create the messageML that is rendering the Table.

When creating a MessageML using a template, you must send a JSON file with it.

Timezone Picker designs

Restricted to the timezones + empty string

Please note that if you want to propose only a list of a few timezones to the users, then a simple might be more adequate.

The possible values of the timezone-picker are restricted to the Canonical values of the .

Date Picker designs
<messageML>
    Here are examples of using emojis: <emoji shortcode="flag_fr" /><emoji shortcode="flag_us" />
</messageML>

Example of a room selector with both a room and a user pre-selected. Please note that pre-selected streamIds must follow

Time Picker designs

Learn more about Desktop Interop and intents .

On click, Symphony will raise the embedded intent.

To containing a Structured object, construct the message content using MessageML with either a <div> or a <span> element containing the following attributes:

Messages sent using the API are translated into equivalent XHTML tags known as PresentationML, that can be rendered and processed by any HTML client. For example, when you create a message using , the message field in the response contains the message in PresentationML format.

Since PresentationML was included as part of the MessageML format design, you can create messages by passing the message content in MessageML or in PresentationML. Note that PresentationML uses rather than shorthand tags. Therefore, when you send a message using , the message parameter must contain the message content in PresentationML XHTML markup, and the data parameter must contain the XML markup for the standard entities referenced in the message.

Structured Objects can be rendered richly using .

Object Presentation, in format.

You can create an object by invoking the endpoint. You need to include:

These parameters also support using with Structured Objects.

You can read objects using any of the endpoints designed to read messages, for example, the endpoint. This endpoint will let you read both the message presentation and object data fields.

As described in , messages with can be created using the shorthand tags or the full tags. When they are read, the message presentation always contain the full tags, which are a subset of HTML tags.

Create an

Your application needs to use the , which will allow you to:

Register your application as being able to render a specific type, using the .

Render the object itself, by implementing the .

Regular expressions can cause performance issues in the Client if the validation of the regular expression is very complex. Poorly designed regular expressions can even cause denial of service (ReDoS). Please verify that your regular expressions are safe, using the following service: .

For more information, refer to .

<ui-action action='open-dialog'> tag. This tag defines where and how the will be displayed, and it references the dialog that will open using the target-id attribute;

The dialog functionality supports in the following way:

Please also note that users can close the dialog thanks to the cross (x) displayed at the top-right corner of it, as well as with a new type of that has been created for that purpose: <button type="cancel">. You can also specify the class attribute of the button which is by default set to "tertiary" for this new button.

Along with any request made to the , bots should send header X-Trace-Id (random alphanumeric string of 6 characters) for logging purposes.

Please note that the Symphony BDK for Java sets up your logger (Mapped Diagnostic Context) with this X-Trace-Id. This is especially useful for cross-applications debugging, assuming that the X-Trace-Id value is also present in your application logs. You can find more information about how to print the X-Trace-Id with specific technologies (like logback or log4j2) in the .

The offers a fast way to bootstrap your Symphony extension app project.

Prerequisite: Install NodeJS first, either or via . You can also use other package managers like or , which the generator will attempt to use before falling back to npm.

If you require an extension app that requires user identity, you will also need a backend that can perform the process. You should then select the Extension App + Circle of Trust (ADK + BDK) option, which will generate both the extension app project using ADK and a backend project using BDK that will perform the app authentication and validation.

A browser window should launch with the URL . If it doesn't, visit that page manually. Dismiss the security warning and close the page.

Visit to inject the running app temporarily into a pod for testing

If you need additional inspiration, checkout our for examples of what has been built by our robust developer community!

The OpenChat functionality is not supported in forms (see ).

Apache FreeMarker
Checkbox
Button
FreeMarker
FreeMarker
Freemarker
tz database
here
Custom Entities
create a message
Create Message v4
Structured Objects
Create Message v4
Symphony's Extension API
MessageML v2
Create Message
Apache FreeMarker templates
Read Message
Message Format - MessageML v2
Structured Objects
Extension application
entity service
URL Safe Base64 encoded StreamID.
<!-- Validates a login -->
<text-field name="login" pattern="^[a-zA-Z]{3,}$" pattern-error-message="Login must contain at least 3 letters."></text-field>

<!-- Validates a decimal value, hint is not defined -->
<text-field name="price" pattern="^\d+,\d+$" pattern-error-message="This is an incorrect value."></text-field>

<!-- Validates a password -->
<text-field name="password" masked="true" pattern="^[a-zA-Z]\w{3,14}$" pattern-error-message="Your password is not strong enough."></text-field>
<!-- Validates that a line does not contain the word "badword" -->
<textarea name="justification" pattern="^((?!badword).)*$" pattern-error-message="Justification text must not contain the word 'badword'"></textarea>

Attribute

Type

Required?

Description

action

String

Yes

For Dialogs, always set to action='open-dialog'.

target-id

String

Yes

Id of the dialog that must be opened when user will trigger this ui-action.

See the id attribute in the <dialog> tag.

Attribute

Type

Required?

Description

id

String

Yes

Id of the dialog that will be triggered thanks to the ui-action it is associated to. See target-id attribute in <ui-action> tag.

width

String

No.

Default to medium

Specifies the width of the dialog.

NB: values can be: small, medium, large, or full-width.

<messageML>
  <form id="wrapping_dialog">
    <h4>First part: form wrapping a dialog</h4>
    <p>Here is the beginning of the form containing the dialog hiding some large text</p>
    <text-field name="input1" label="This is an interactive Element in the form but outside of the dialog" />
    <p>Please click on the following button in order to open the dialog and see the hidden information</p>
    <ui-action action="open-dialog" target-id="dialog_in_form">
      <button class="secondary">Open the dialog contained in the form</button>
    </ui-action>
    <text-field name="input2" label="This is an interactive Element in the form but outside of the dialog" />
    <button name="wrapping_dialog">Submit Button of the Form</button>
    <dialog id="dialog_in_form">
      <title>
        This is a title
      </title>
      <body>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam congue viverra interdum. Integer quam odio, gravida ultricies pharetra ac, tempor eget leo. Duis vitae arcu sed turpis faucibus feugiat a at ipsum. Nulla facilisi. Phasellus egestas, leo et malesuada porttitor, felis turpis viverra tortor, quis suscipit orci nibh at augue. Curabitur erat libero, accumsan vitae ipsum eleifend, tincidunt bibendum purus. Curabitur ultricies lorem tincidunt rutrum viverra. Sed mattis dui at suscipit auctor. In rutrum neque urna, vitae lacinia turpis blandit eget. Nullam eu dignissim purus. Sed ut ante lorem. Duis quis mi nec enim imperdiet consectetur. Phasellus aliquet accumsan ipsum non ullamcorper.<br/><br/>
Praesent convallis odio tortor, sit amet vulputate nulla tincidunt vitae. Donec ultrices eros suscipit mauris condimentum iaculis. Ut posuere finibus quam a consequat. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nam aliquet dapibus vehicula. Nunc vel lectus congue, finibus felis ut, laoreet est. Maecenas eleifend gravida metus, nec viverra mi egestas eu. Pellentesque scelerisque mattis nibh, eu condimentum nulla finibus ut.<br/><br/>
Ut dignissim varius libero ac volutpat. Sed hendrerit nec libero ut ullamcorper. Nunc et risus sed purus luctus faucibus nec eu est. Praesent id justo ante. Sed sed enim velit. Ut ac mauris magna. Fusce bibendum ullamcorper diam quis semper. Aenean mattis auctor ultricies. Mauris dui enim, vehicula sit amet finibus non, consectetur eu ante. Fusce at mi a ipsum gravida rhoncus.<br/><br/>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam congue viverra interdum. Integer quam odio, gravida ultricies pharetra ac, tempor eget leo. Duis vitae arcu sed turpis faucibus feugiat a at ipsum. Nulla facilisi. Phasellus egestas, leo et malesuada porttitor, felis turpis viverra tortor, quis suscipit orci nibh at augue. Curabitur erat libero, accumsan vitae ipsum eleifend, tincidunt bibendum purus. Curabitur ultricies lorem tincidunt rutrum viverra. Sed mattis dui at suscipit auctor. In rutrum neque urna, vitae lacinia turpis blandit eget. Nullam eu dignissim purus. Sed ut ante lorem. Duis quis mi nec enim imperdiet consectetur. Phasellus aliquet accumsan ipsum non ullamcorper.<br/><br/>
Praesent convallis odio tortor, sit amet vulputate nulla tincidunt vitae. Donec ultrices eros suscipit mauris condimentum iaculis. Ut posuere finibus quam a consequat. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nam aliquet dapibus vehicula. Nunc vel lectus congue, finibus felis ut, laoreet est. Maecenas eleifend gravida metus, nec viverra mi egestas eu. Pellentesque scelerisque mattis nibh, eu condimentum nulla finibus ut.<br/><br/>
Ut dignissim varius libero ac volutpat. Sed hendrerit nec libero ut ullamcorper. Nunc et risus sed purus luctus faucibus nec eu est. Praesent id justo ante. Sed sed enim velit. Ut ac mauris magna. Fusce bibendum ullamcorper diam quis semper. Aenean mattis auctor ultricies. Mauris dui enim, vehicula sit amet finibus non, consectetur eu ante. Fusce at mi a ipsum gravida rhoncus.
      </body>
      <footer>
        This is a footer
        <button name="cancel1" type="cancel">Close</button>
      </footer>
    </dialog>
  </form>
  <br/>
  <br/>
  <h4>Second part: message with a dialog hiding a form</h4>
  <p>Here is the beginning of the message containing the dialog hiding some large form</p>
  <dialog id="dialog_containing_form" width="large">
    <form id="wrapped_in_dialog">
      <title>
        This is a title
      </title>
      <body>
        <text-field name="input3" label="This is an interactive Element in the form which is contained in the dialog" />
        <textarea name="textarea" label="Other interactive Element in the dialog" />
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam congue viverra interdum. Integer quam odio, gravida ultricies pharetra ac, tempor eget leo. Duis vitae arcu sed turpis faucibus feugiat a at ipsum. Nulla facilisi. Phasellus egestas, leo et malesuada porttitor, felis turpis viverra tortor, quis suscipit orci nibh at augue. Curabitur erat libero, accumsan vitae ipsum eleifend, tincidunt bibendum purus. Curabitur ultricies lorem tincidunt rutrum viverra. Sed mattis dui at suscipit auctor. In rutrum neque urna, vitae lacinia turpis blandit eget. Nullam eu dignissim purus. Sed ut ante lorem. Duis quis mi nec enim imperdiet consectetur. Phasellus aliquet accumsan ipsum non ullamcorper.<br/><br/>
Praesent convallis odio tortor, sit amet vulputate nulla tincidunt vitae. Donec ultrices eros suscipit mauris condimentum iaculis. Ut posuere finibus quam a consequat. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nam aliquet dapibus vehicula. Nunc vel lectus congue, finibus felis ut, laoreet est. Maecenas eleifend gravida metus, nec viverra mi egestas eu. Pellentesque scelerisque mattis nibh, eu condimentum nulla finibus ut.<br/><br/>
Ut dignissim varius libero ac volutpat. Sed hendrerit nec libero ut ullamcorper. Nunc et risus sed purus luctus faucibus nec eu est. Praesent id justo ante. Sed sed enim velit. Ut ac mauris magna. Fusce bibendum ullamcorper diam quis semper. Aenean mattis auctor ultricies. Mauris dui enim, vehicula sit amet finibus non, consectetur eu ante. Fusce at mi a ipsum gravida rhoncus.<br/><br/>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam congue viverra interdum. Integer quam odio, gravida ultricies pharetra ac, tempor eget leo. Duis vitae arcu sed turpis faucibus feugiat a at ipsum. Nulla facilisi. Phasellus egestas, leo et malesuada porttitor, felis turpis viverra tortor, quis suscipit orci nibh at augue. Curabitur erat libero, accumsan vitae ipsum eleifend, tincidunt bibendum purus. Curabitur ultricies lorem tincidunt rutrum viverra. Sed mattis dui at suscipit auctor. In rutrum neque urna, vitae lacinia turpis blandit eget. Nullam eu dignissim purus. Sed ut ante lorem. Duis quis mi nec enim imperdiet consectetur. Phasellus aliquet accumsan ipsum non ullamcorper.<br/><br/>
Praesent convallis odio tortor, sit amet vulputate nulla tincidunt vitae. Donec ultrices eros suscipit mauris condimentum iaculis. Ut posuere finibus quam a consequat. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nam aliquet dapibus vehicula. Nunc vel lectus congue, finibus felis ut, laoreet est. Maecenas eleifend gravida metus, nec viverra mi egestas eu. Pellentesque scelerisque mattis nibh, eu condimentum nulla finibus ut.<br/><br/>
Ut dignissim varius libero ac volutpat. Sed hendrerit nec libero ut ullamcorper. Nunc et risus sed purus luctus faucibus nec eu est. Praesent id justo ante. Sed sed enim velit. Ut ac mauris magna. Fusce bibendum ullamcorper diam quis semper. Aenean mattis auctor ultricies. Mauris dui enim, vehicula sit amet finibus non, consectetur eu ante. Fusce at mi a ipsum gravida rhoncus.
      </body>
      <footer>
        <button name="cancel2" type="cancel">Close</button>
        <button name="wrapping_dialog">Submit Button of the Form</button>
      </footer>
    </form>
  </dialog>
  <ui-action action="open-dialog" target-id="dialog_containing_form">
    <button class="secondary">Open the dialog containing the form</button>
  </ui-action>
</messageML>
<messageML>
    <ui-action trigger="click" action="open-dialog" target-id="dialogId">
        <button>Simple dialog</button>
    </ui-action>
    <dialog id="dialogId">
        <form id="formId">
            <title>A Simple dialog</title>
            <body>
                This is a simple dialog.
            </body>
            <footer>
                <button type="cancel" name="cancel-button">Close</button>
            </footer>
        </form>
    </dialog>
</messageML>
<messageML>
    <ui-action trigger="click" action="open-dialog" target-id="dialogId">
        <button>Open the Dialog</button>
    </ui-action>
    <dialog id="dialogId">
        <form id="formId">
            <title>My Form in a Dialog</title>
            <body>
                <text-field name="input" />
            </body>
            <footer>
                <button type="action" name="send-form">Submit</button>
                <button type="reset">Reset</button>
                <button type="cancel" name="cancel-form">Close</button>
            </footer>
        </form>
    </dialog>
</messageML>
<messageML>
    <form id="formId">
        <text-field name="input" />
        <ui-action trigger="click" action="open-dialog" target-id="dialogId">
            <button>Open the Dialog</button>
        </ui-action>
        <dialog id="dialogId" width="small">
            <title>A Dialog</title>
            <body>
                <expandable-card state="expanded">
                    <header>Dialog can include any other messageML component except Interactive Elements Forms</header>
                    <body>This does not contain any interactive element</body>
                </expandable-card>
            </body>
            <footer>
                Some text in the footer
                <button type="cancel" name="cancel-form">Close</button>
            </footer>
        </dialog>
        <h3>Actions</h3>
        <button name="send-answers" type="action">Submit</button>
        <button type="reset">Reset Data</button>
    </form>
</messageML>

Main features introduced

Agent needed to parse message sent by the bot

Client 2.0 release

Client 1.5 release

Backward client-compatibility behavior (e.g. external rooms)

Initial release

20.12.2

21.10

Not working

Not working - entire message is not rendered

here
$ npm i -g yo @finos/generator-symphony
$ yarn global add yo @finos/generator-symphony
$ bun add -g yo @finos/generator-symphony
$ mkdir my-app && cd $_
$ yo @finos/symphony
 __   __     ___                 _
 \ \ / /__  / __|_  _ _ __  _ __| |_  ___ _ _ _  _
  \ V / _ \ \__ \ || | '  \| '_ \ ' \/ _ \ ' \ || |
   |_|\___/ |___/\_, |_|_|_| .__/_||_\___/_||_\_, |
                 |__/      |_|                |__/

Welcome to Symphony Generator v2.9.0
Project files will be generated in folder: /Users/user/code/my-app
______________________________________________________________________________________________________
? Enter your pod host develop2.symphony.com
? Select your project type Extension App (ADK)
? Enter your app id my-app
? Select your application type Basic

Using ADK Version: 1.3.0
   create package.json
   create bundle.json
   create webpack.config.js
   create src/index.js

bun install v1.0.3 (25e69c71)
 + @symphony-ui/adk-webpack@1.3.0
 + webpack@5.88.2
 + webpack-cli@5.1.4
 + webpack-dev-server@4.15.1
 + @symphony-ui/adk@1.3.0

 356 packages installed [3.91s]

Your Extension App project has been successfully generated!

To launch your project, first run: bun start
Then, visit https://localhost:4000/controller.html and dismiss the warning
Finally, visit https://develop2.symphony.com/?bundle=https://localhost:4000/bundle.json
$ npm start # or yarn start or bun start

$ webpack-dev-server --mode=development
SYMPHONY ADK 1.3.0
Building application: my-app
Running at: /Users/user/code/my-app
<i> [webpack-dev-server] Generating SSL certificate...
<i> [webpack-dev-server] SSL certificate: /Users/user/code/my-app/node_modules/.cache/webpack-dev-server/server.pem
<i> [webpack-dev-server] Project is running at:
<i> [webpack-dev-server] Loopback: https://localhost:4000/
<i> [webpack-dev-server] On Your Network (IPv4): https://192.168.1.2:4000/
<i> [webpack-dev-server] On Your Network (IPv6): https://[fe80::1]:4000/
<i> [webpack-dev-server] Content not from webpack is served from '/Users/user/code/my-app/node_modules/@symphony-ui/adk-webpack/dist' directory
Entrypoint controller 450 KiB = vendors.c3acceed.js 404 KiB controller.26a834b5.js 46.4 KiB
webpack compiled in 750 ms
<messageML>
  <ui-action action='open-im' stream-id='rKiuGRoGDTrPtpHSIgmUgH///oVFs7kzdA=='><button>Stream</button></ui-action>
  <ui-action action='open-im' trigger='click' user-ids='[13056700583037]'><button class='secondary'>User A</button></ui-action>
  <ui-action action='open-im' trigger='click' user-ids='[13056700583037,13056700583039]'><button class='tertiary'>List of Users</button></ui-action>
  <ui-action action='open-im' trigger='click' stream-id='rKiuGRoGDTrPtpHSIgmUgH///oVFs7kzdA==' side-by-side='false'><button class='destructive'>Replace current chat</button></ui-action>
</messageML>

Main features introduced

Agent needed to parse message sent by the bot

Client 2.0 release

Client 1.5 release

Backward client-compatibility behavior (e.g. external rooms)

Initial release

20.12.2

21.7

20.13

Not working

Add an Extension App to a Symphony Pod

Create Extension App for Production

This section is meant as a reference for pod administrators. If you are a developer trying to load your extension app onto your company's pod, please seek assistance from your pod administrator or local IT helpdesk.

  1. Visit the Admin and Compliance Portal of the respective pod either via Settings > Admin Portal in the web or desktop client or by visiting https://my-company-pod.symphony.com/?admin

  2. From the left navigation, select App Management

  3. Click on Add Custom App on the top right

  4. If the app developer has provided a bundle.json file, use the Import Application Bundle File button on the top right. If not, fill in the fields manually.

  5. Ensure that the RSA public key and required app permissions are set correctly

  6. Click Create

  7. From the left navigation, select App Settings

  8. Locate the newly-created entry and change the Global Status from Disabled to Enabled

  9. If the app is intended for users to optionally self-install and uninstall from marketplace, change the Visibility from Hidden to Visible

  10. If the app is intended for all users in the organisation to have installed, change the Installation from Manual to Automatic

  11. Click Save at the bottom

Sideload Extension App for Development or Testing

Note that this method does not support obtaining user identity as app authentication is not supported. If you wish to test an app that requires user identity, you will have to create an actual app using the steps above with the assistance of your pod administrator

  1. Host the extension app on a TLS-enabled server together with the application manifest (bundle.json file)

  2. Visit https://my-company-pod.symphony.com/?bundle=https://localhost:4000/bundle.json assuming your app is running on localhost on port 4000 and you have a bundle.json served on the root

  3. Acknowledge the developer mode warning and proceed

🤝
Buttons
Checkboxes
tz database
dropdown menu
Unix Epoch Timestamp
https://redos-checker.surge.sh/
Regular expression Denial of Service - ReDoS
button
Interactive Elements Forms
button
Symphony APIs
MDC
BDK 2.0 for Java documentation
Planning Your App
Overview of Extension API
Symphony Generator
directly
nvm
bun
yarn
Circle of Trust
Add an Extension App to a Symphony Pod
https://localhost:4000/controller.html
https://develop2.symphony.com/?bundle=https://localhost:4000/bundle.json
Build a Basic Extension App
Build an Extension App with App Views
Build an Extension App with Message Renderers
Overview of Extension API
Symphony App Directory
App Authentication
Add Buttons and Handlers to an Extension App
Build an Extension App with Message Renderers
Build an Extension App with App Views
Symphony Elements

Property

Value

Language

Javascript (NodeJS)

Author

Symphony

License

MIT

Source Code

Property

Value

Language

JavaScript (Node)

Author

Symphony

License

MIT

Source Code

Property

Value

Language

Java

Author

Symphony

License

MIT

Source Code

Property

Value

Language

Java

Author

Symphony

Licence

MIT

Source Code

Attribute

Type

Required?

Description

action

String

Yes

For OpenChat, always set to action='open-im'

user-ids

List of Integers

Exclusive with stream-id

List of ids of the users with whom the MIM must be opened when the Symphony user triggers the OpenChat functionality.

stream-id

String

Exclusive with user-ids

The id of the room/IM that must be opened when the Symphony user triggers the OpenChat functionality.

side-by-side

Boolean

No.

Default to true

If set to false, the current chat will be replaced by the new chat to be opened when the Symphony user triggers the OpenChat functionality.

Extension API Services

The Client Extension API uses services for communication between your application and the Symphony client. The Client Extension API provides a set of services that your application can leverage to extend the Symphony Client and to create custom workflows and experiences.

Client Extension API Services:

The Client Extensions API provides the following remote services:

Modules Service

Use the modules service to create application-specific modules:

Applications-Nav Service

The Applications navigation section is found at the bottom of the left-hand sidebar of the Symphony client workspace. Use the applications-nav service to create a navigation item for your application:

UI Service

Use the ui service to extend various parts of the Symphony client user interface. For example, add buttons on 1 to 1 chats and chatroom modules or add links to the #hashtag and $cashtag hovercards:

Share Service

Use the share service to allow users to share content from your application into Symphony conversations:

Entity Service

Use the entity service to allow your app to render a Structured Object within a within a message sent by the REST API:

Commerce Service

Apps can offer premium functionality through licensed subscriptions. Use the commerce service to identify the products (premium versions) to which a user is subscribed:

Dialogs Service

Use the dialogs service to create modal windows (e.g. to open a modal window from a button registered at a room level):

Service Interface

Both the Client Extensions API services and your application services use the same interface. Continue here to learn how to implement the Service interface methods:

Register and Connect

SYMPHONY.application.register()

Register an application controller with the Symphony client. Additionally, subscribe the application to remote services and register local services that can be used remotely. Returns a promise that will be fulfilled when registration is complete.

register : function(id, servicesWanted, servicesSent)

Parameter

Type

Description

id

String

The id of your application. For partner apps, this is an alphanumeric string chosen by the partner. For custom enterprise apps, this is generated when creating the app in the Admin Portal.

servicesWanted

Array of Strings

A list of names of remote services that your application wants to subscribe to

servicesSent

Array of Strings

A list of names of local services your application wants to make available remotely (any implemented methods on this service will be made available remotely)

Returns

Type

Description

userReferenceId

String

A unique anonymized identifier for the user in context that will perpetuate until the user uninstalls the application

// Register the "hello" application with the Symphony client
// Subscribe our application to Symphony's services
// Register the "hello" app's controller service (this service must be registered using SYMPHONY.services.register())
SYMPHONY.application.register(
  "hello",
  ["modules", "applications-nav", "ui", "share", "commerce"],
  ["hello:controller"]
).then(function(response) {
  var userId = response.userReferenceId;
}));

SYMPHONY.application.connect()

Connect an application view to an existing application that has been registered with Symphony. Additionally, subscribe the application to remote services and register local services that can be used remotely. Returns a promise that will be fulfilled when connection is complete.

connect : function(id, servicesWanted, servicesSent)

Parameter

Type

Description

id

String

The id of your application. For partner apps, this is an alphanumeric string chosen by the partner. For custom enterprise apps, this is generated when creating the app in the Admin Portal.

servicesWanted

Array of Strings

A list of names of remote services that your application wants to subscribe to

servicesSent

Array of Strings

A list of names of local services your application wants to make available remotely (any implemented methods on this service will be made available remotely)

Returns

Type

Description

userReferenceId

String

A unique anonymized identifier for the user in context that will perpetuate until the user uninstalls the application

// Connect an application view to the "hello" application
// Subscribe our application to Symphony's services
// Register the "hello" app's view service (this service must be registered using SYMPHONY.services.register())
SYMPHONY.application.connect(
  "hello",
  ["modules", "applications-nav", "ui", "share", "commerce"],
  ["hello:app"]
).then(function(response) {
  var userId = response.userReferenceId;
}));

Application Manifest Bundle

Each application's metadata is represented by a manifest file called bundle.json. You must create a manifest file for your application and submit it to either your pod administrator for internal apps, or Symphony for apps meant for the public marketplace.

To create and upload a file:

  1. Create the manifest file that your application requires.

  2. Upload the manifest file to Symphony: drag and drop the file into the application creation window of the administrative portal.

Application Manifest Bundle File Sample

The following bundle file is only applied for Developer Mode. Note that it is an array of apps, allowing you to load multiple apps at once.

{
  "applications": [
    {
      "type": "sandbox",
      "id": "hello",
      "name": "Hello World",
      "blurb": "This is a hello world app with a few example extension API invocations!",
      "publisher": "Symphony",
      "url": "https://localhost:4000/controller.html",
      "domain": "localhost",
      "icon": "https://localhost:4000/icon.png"
    }
  ]
}

AC Portal x Developer Mode

In the following examples, the bundle files are applied for the AC Portal. Note that they are different than the one applied for Developer Mode.

{
      "appGroupId": "testapplication",
      "name": "Application name",
      "description": "Enter the application description, as it will appear in the Symphony Market",
      "publisher": "Symphony",
      "loadUrl": "https://symphony.myapplication.com:4000/controller.html",
      "domain": ".myapplication.com",
      "iconUrl": "https://symphony.myapplication.com/icon.png",
      "notification": {
         "url": "https://symphony.myapplication.com:4000/podInfo",
         "apiKey": "super-secret-key" 
      },  
      "permissions": [
        "ACT_AS_USER",
        "GET_BASIC_CONTACT_INFO",
        "GET_PRESENCE"],
      "certificate": "-----BEGIN CERTIFICATE-----\nMIICpTCCAY2gAwIBAgIBATANBgkqhk...sCPV2jH\n0hFUK5JHPrGO\n-----END CERTIFICATE-----\n",
      "allowOrigins": "myapplication.com",
      "rsaKey": "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA1QGTGazbI/\n-----END PUBLIC KEY-----"
    }
{
    "applicationInfo": {
        "appId": "my-test-app",
        "name": "my-test-app-update",
        "appUrl": "https://joe.mydomain.com",
        "domain": "mydomain.com",
        "publisher": "Updated Joe Smith"
    },
    "description": "updating an app",
    "allowOrigins": "mydomain.com",
    "iconUrl": "https://symphony.myapplication.com/icon.png",
    "permissions": [
        "ACT_AS_USER",
        "SEND_MESSAGES"
    ],
    "cert": "certificate",
    "authenticationKeys": {
      "current": {
         "key":"-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0....YOUR_NEW_KEY...cCAwEAAQ==\n-----END PUBLIC KEY-----"
    }

Fields details

The table describes the required and additional optional fields.

Field

Description

Required

Type

appGroupId

Applied for version 1.53 onwards. The unique identifier for your app. The ID must not match any existing app IDs or user names.

It consists of alphanumeric characters [0-9,A-Z, a-z], underscores (_), and dashes.

Note: Do not use a colon (:) in this field, or you will receive a 401 Unauthorized error.

Required

String

name

The name of your app, which will be displayed in the Symphony Market.

Required

String

description

The description of your app, which will be displayed in the Symphony Market.

Optional

String

publisher

The publisher of your app, which will be displayed in the Symphony Market.

Optional

String

loadUrl

The location of the app's controller, which will be displayed in a hidden iframe. Value must start with https://.

Note: Do not specify this value for On Behalf Of (OBO) applications.

Required, except for OBO applications

URI

domain

The domain for your app, which should match the controller file URL.

Required

URI

iconUrl

An icon for your app (32x32 px), which will be displayed in the Symphony Market.

Optional

URI

notification

Fields required in order to receive webhook callback containing pod information (ie. pod, authentication and Agent URLs).

Optional

Object

url

URL which the pod will call to send pod information to the application

Optional

String

apiKey

Secret key used to validate the pod when calling the notification URL.

Optional

String

permissions

Optional

List

certificate

Optional

String

rsaKey

Optional

String

allowOrigins

The origin or origins that this application is allowed to receive cross-domain requests from.

Optional

URI

type

Applied for 1.52 and prior versions.This field should be set to sandbox, which indicates that this is a standalone app, embedded within Symphony's client.

Required

String

id

Applied for 1.52 and prior versions. The unique identifier for your app. Note: Do not use a colon (:) in this field, or you will receive a 401 Unauthorized error.

Required

String

blurb

Applied for Developer Mode. Field for display in the Symphony Market and Admin Portal.

Optional

String

icon

Applied for Developer Mode. An icon for your app (32x32 px), which will be displayed in the Symphony Market.

Optional

String

Overview of Extension API

Symphony extension applications are standalone web applications that are embedded within the Symphony user interface as iframes that interact with the Symphony container using the Client Extension API.

Extension API Capabilities

The Client Extension API is a JavaScript library that consists of services containing methods that allow developers to build apps that extend and interact with Symphony's user interface. Using these services, developers can:

  • Add modules, or windows, containing your app content to the Symphony client

  • Add entry points for your app, such as navigation items in Symphony's left sidebar or links on #hashtag and $cashtag hovercards

  • Add interactive buttons to chat and user profile module headers

  • Enable users to share content from your app into Symphony chats

  • Register custom renderers to richly display messages containing structured objects

Some of Symphony’s services will require you to implement your own services with methods to handle events. For example:

  • Handling a user click on your app’s left sidebar menu item by opening your default app module

  • Handling a user click on your app’s #hashtag or $cashtag hovercard link by opening an app module with a contextual search

App Controller and Views

Applications created with the Client Extension API run in iframes inside the Symphony client.

Symphony apps consists of:

  • The main application controller, a hidden iframe that uses the Client Extension API services to bootstrap your app, extending the Symphony user interface

  • In most cases, one or more application views, separate iframes that are rendered within Symphony modules

Applications can be built using any web development technology of your choice.

Extension API Services

Services are used for communication between your app and the Symphony client. There are two types of services: remote and local.

Remote Services

Remote services are services that are shared:

  • The services of the Client Extension API are remote services whose methods can be invoked by your application controller and views

  • You can also implement remote services that can be shared between your application controllers and views

Local Services

Local services are services are specific to either your controller or one of your views.

To learn more about the services and capabilities provided by the Extension API continue here:

Initialization

Including the Client Extension API Javascript Library

To use the Client Extension API services, you must include the symphony-api.js JavaScript file in your application controller and views.

<script type="text/javascript" src="https://cdn.symphony.com/resources/api/v1.0/symphony-api.js" charset="utf-8"></script>

Including Symphony's Style Sheet

To style your app, you must include the symphony-style.css CSS file in your application views and add the class "symphony-external-app" to the <body> tag of your app views.

<link rel="stylesheet" type="text/css" href="https://cdn.symphony.com/resources/api/v1.1/symphony-style.css">

Icons guidelines

Icons must be square, with a recommended size of 32x32.

The supported formats are SVG (recommended), PNG and JPG.

Transparency is supported, but please take into account that your icon should work both in the light theme and the dark theme.

In the legacy Symphony Client, it was possible to use a rectangular sprite icon (32x16), where the first 16 pixels were used for the light theme and the last 16 pixels for the dark theme. This is not supported on Client 2.0.

SYMPHONY.remote.hello()

The SYMPHONY.remote.hello() method should be used to initialize the connection to the Client Extension API from your application controller and views.

Returns a promise that will be fulfilled when the introduction is complete. If there is a problem, the promise will be rejected. The promise returns an object containing the user's Symphony client theme name, font size, and any associated classes, including those for theme name or size, as well for condensed and contrast modes.

hello: function()

Returns

Type

Description

themeV2

Object

An object containing the user's Symphony client theme settings

locale

String

The language selected by the user in his settings. Possible values are "en-US" | "fr-FR" | "ja-JP".

{
  "themeV2" : {
    "name": "dark",
    "size": "normal",
    // This will include a list of all theme and font classes available.
    "classes": [],
  },
  "locale" : "en-US"
}

You should style your application according to the user's theme by applying the theme and font size classes to the <body> tag of any application modules.


  SYMPHONY.remote.hello().then(function(data) {
  // Set the theme of the app module
  var themeColor = data.themeV2.name;
  var themeSize = data.themeV2.size;
  // You must add the symphony-external-app class to the body element
  document.body.className = "symphony-external-app " + themeColor + " " + themeSize;
});

Entity Service

Use the entity service to allow your app to render a Structured Object created by the REST API within a message:

The following methods are available on the entity service:

registerRenderer()

Register a renderer for a type of entity:

You must implement the render() method on the specified application service. This method will be invoked when the associated entity is rendered in the Symphony client.

render()

Renders an entity given its type and data values:

The render method returns an object with the following fields:

update()

Effectively re-renders an entity with new template and data objects given its entityInstanceId:

Sample Renderer Application

pause() and resume()

The 'pause' and 'resume' methods are optional. If you choose to use the methods, implement them on the renderer service. These methods will be invoked when the associated entity is rendered in the Symphony client.

Entities are checked periodically (every two seconds) if they are visible on the screen. If an entity is completely visible, a resume event is triggered to the renderer service of this entity. If an entity is partially visible or completely hidden, a pause event is triggered instead.

Example: • Pause event: responsible for stopping/pausing a video transmission (video iframe). • Resume event: responsible for playing the video.

Note: An entity is considered not visible when scrolling the screen down/up or when changing the module (conversation) to a new chat room or screen.

If the entity has an iframeId, it will be passed in the invocation of the methods, otherwise the entityInstanceId will be used instead.

iFrame Tag Support

Register and Subscribe

In order to leverage the services provided by the Client Extension API, you must first subscribe to them. To use your applications own services, you must register them via the Client Extension API. Extension apps can register and subscribe to local and remote services in the following ways:

SYMPHONY.services.make()

Creates a new local service and register it using existing classes:

If you are developing an Object-Oriented Application, SYMPHONY.services.make() allows you to use a class as a prototype for implementing service methods, allowing you to choose which methods of the class will be available in the service by specifying them in the implements list.

This cannot be and this cannot be achieved by the SYMPHONY.services.register() function, which is only recommended when creating small services since it does not require any class, and it only requires the serviceName. If you use SYMPHONY.services.register() for large services, you will have to call service.implement() passing an object to it. As a result, all the methods will be public and the code might look unorganized.

SYMPHONY.services.register()

Creates a new local service and register it to be used by a specific application view or your application controller:

SYMPHONY.services.register() is an alternative method for creating a new local service and it is recommended only when creating small services. For Object-Oriented Applications, the use of the SYMPHONY.services.make() is recommended since it uses the class as a prototype for implementing the service methods.

Local services should be namespaced using a string representing the application followed by a colon. For example: hello:controller.

SYMPHONY.services.subscribe()

Finds a service - either local or remote - that has been registered and returns it. Returns false if the service does not exist.

In order to use a service, it must have been requested by your application during application.register() or application.connect() .

SYMPHONY.remote.register()

Takes an existing local service and makes it available remotely. Use this to make services available between multiple application views or between your application view and controller:

SYMPHONY.remote.subscribe()

Imports a remote service and makes it available locally. Returns a promise which resolves to a reference to a service. This service would now be available in your registry using SYMPHONY.services.subscribe:

Service Interface

Both the Client Extensions API services and your application services use the same interface. The service interface consists of the following methods:

implement()

Create a method on a service and specify the implementation.

Alternately, create several methods on a service at once by specifying an object consisting of multiple functions:

invoke()

Call a method on a service. Any extra parameters passed to this method will be sent as arguments to the service method:

fire()

Fire an event from a service. Any extra parameters passed to this method will be passed to the callbacks defined by services that listen to this event:

listen()

Subscribe a service to an event that is fired by another service (or itself). This method returns a handle that can later be used to remove the listener:

The listen namespace is the same as the namespace for methods, thus names must be unique across both service methods and events.

remove()

Unsubscribe a service from an event:

Modules Service

A module is a new window inside the Symphony client workspace, such as a chatroom or an instant message. Use the modules service to create application-specific modules.

The following methods are available on the modules service:

show()

Show a new application module:

hide()

Hide an existing application module:

setTitle()

Change the title of an existing application module:

Note that this only changes the title of a specific module, not all titles of all modules created by the application.

focus()

Focus an existing application module:

openLink()

Opens a link from your application in a new tab in the user's default browser. This method should be used to open links, rather than <a href="..." target="_blank">...</a>.

redirect()

Reloads the content of the module at the new URL.

The Client Extensions API is designed for single-page applications. Use this method with multi-page applications to load new content when users navigate to another page:

Message Format - ExtensionML

ExtensionML is a special form of markup that a front end app can use to perform custom rendering of an entity, instead of relying on Symphony to perform the default presentation.

While similar to PresentationML, ExtensionML consists of a template and corresponding data. Each tag in the template can have an ID that references a value stored in the data, binding the data to whatever is being rendered. For example, multiple paragraphs in the template can reference a sentence stored in the data by ID, allowing for reuse of that sentence in multiple places within the template being rendered.

Symphony Elements

Standard HTML Tags

A number of standard HTML tags are supported within templates: b, u, i, strong, br, ul, ol, li, span, div, table, th, tr, and td.

These behave like their HTML counterparts but must be properly-formatted XML. So, for example, rather than <br> you must use <br/>.

Text Tags

The following tags present text in different ways. Most of these require data to specify their content, but some can also use the content between the opening and closing tags.

Extended HTML Tags

The following HTML tags are handled in a slightly modified way:

Special Entity Tags

Flow Control Tags

The following flow control tags are used for entities that have conditional logic or data that can be iterated upon:

The following example shows the XML template for an entity with flow control logic and corresponding data:

Generating Properly Formatted XML

The following function can be used to turn HTML into properly formatted XML:

Entity Advanced Templating

For a full ExtensionML reference, continue here:

Template and Data

You can create a reusable template that leverages the values in the data object for the specific object. When using the data for these attributes, the template uses the id attribute on the tag to specify which member of the data object holds these attributes.

For example, the template and data below could be used to display a link within a Symphony message:

Flow Control Tags

Use the following flow control tags to support entities that have conditional logic or recursive data:

Interactivity Tags

Use the following tags to have interactivity on messages by implementing methods that are called to execute business logic:

Using Actions

The following steps show examples on how to use actions.

  1. Add the <action> tag to an entity template.

  1. The data field of the entity must have objects that match the actions ids. For these objects, we can provide an icon, a label and a service name for the action.

  1. Implement an action method for the service of the entity renderer. This method will be called once the <action> tag is clicked.

Commerce Service

Apps can offer premium functionality through licensed subscriptions. Use the commerce service to identify the products (premium versions) to which a user is subscribed:

getProducts()

Returns the list of products to which the user is subscribed for your app:

This method returns a promise that will be fulfilled with the array of products the user is subscribed to for your app. For each product, the following is returned:

"Premium" is the term used for paid subscription products, while "Standard" represents a freemium app. The name and SKU will be values specified by the application developer when implementing a premium version of their app.

productUpdate

This event is fired when the user's product subscriptions are changed.

Applications-Nav Service

The Applications navigation section is found at the bottom of the left-hand sidebar of the Symphony client workspace. Use the applications-nav service to create a navigation item for your application:

The following methods are available on the applications-nav service:

  • add

  • remove

  • count

  • rename

  • focus

add()

Add a new navigation item to the Applications section of the left-hand sidebar:

Note: You must implement the select method on your application service in order to handle clicks on the created left navigation item.

remove()

Remove an existing application navigation item:

count()

Set the badge (notification) count on an application navigation item:

rename()

Change the title of an existing application navigation item.

Note that this only changes the title of a specific navigation item -- not to all navigation items created by the application:

focus()

Focus an existing application navigation item:

NB: Please note that the stream-id is the exact Conversation ID that you can find in the UI, and not the URLSafe64 converted id (see more details in )

You must register your application controller with Symphony and connect your application views using the SYMPHONY.application methods. During this time, the that will be used by your application must be specified.

This method must be called before the application can to any services.

List of permissions the application requires. See Permissions details for .

Certificate for the application. See

Applied for version 1.54 onwards. RSA key for the application. See

For more information, see the AllowedOrigin description in .

Many of these event handlers are provided out of the box by the BDK 1.0's App Template. To learn more about the out-of-the-box implementations provided by the BDK 1.0 continue to the or sections.

If a user changes his theme, a themeChangeV2 event is fired from the , which will pass a themeV2 object with the new values. You should use a service to to this event and update the classes on the application module <body>.

In addition to tags, iframe tags are also supported in the template parameter:

ExtensionML is generated from a given entity and emitted by a built-in or third-party renderer. It is similar to but is used for interactive presentation.

Note: ExtensionML does not support Symphony Elements. For more information, refer to .

The templateparameter in the render() function of the is an string used to render entities. To render a static object or an iFrame, you can use advanced templating.

Use the method on the service specified in getProducts() to subscribe to this event and specify a callback that will be executed when the event is fired. The callback should change the contents/features of your application to match the user's updated product subscriptions.

https://github.com/sym-bizops-bots/symphony-exporter-public
https://github.com/SymphonyPlatformSolutions/symphony-roomzilla-bot
https://github.com/SymphonyPlatformSolutions/symphony-trade-buddy-bot
https://github.com/SymphonyPlatformSolutions/symphony-poll-bot-java
Overview of Streams
Modules Service
Applications-Nav Service
UI Service
Share Service
Entity Service
Commerce Service
Dialogs Service
Service Interface
Services
register or subscribe
Planning Your App
Tutorials
Extension API Services
var entityService = SYMPHONY.services.subscribe("entity");
function registerRenderer(type, options, serviceName)

Parameter

Type

Description

type

String

The type of entity that will be rendered by your application. Entities are namespaced using reverse domain name notation, e.g. com.symphony.address.

options

Array

Reserved for future use.

serviceName

String

The name of the application service that will be used to render this entity.

render: function(type, data) {}

Parameter

Type

Description

type

String

The type of entity to be rendered.

data

Object

The data for the specific entity being rendered. This data is specified when the message is created.

update: function(entityInstanceId, template, data) {}
// The application service that will be used to handle entity renderering
const helloControllerService = SYMPHONY.services.register("hello:controller");

const entityService = SYMPHONY.services.subscribe("entity");
entityService.registerRenderer(
  "com.symphony.address",
  {},
  "hello:controller"
);

// Implement the trigger method on your application service
helloControllerService.implement({
  render: (type, data) => {
    const template = "<entity><span>Street Address: <text id='address'/></span><br/><span>City: Palo Alto</span><br/><span>State: California</span><br/><span>Zip Code: 94304</span></entity>"
    entityInstanceId = "0";

    if (type == "com.symphony.address") {
      const newTemplate = "<entity><span>The message is updated</span></entity>";
      // Update the entity after 5 seconds with a new template
      setTimeout(() => {
        entityService.update(entityInstanceId, newTemplate, {});
      }, 5000);

      return {
        template,
        data,
        entityInstanceId
      };
    }
  }
});
helloControllerService.implement({
  render: function(type, data) {
    if (type == "com.symphony.address") {
      return {
        template: '<entity><iframe src="http://your-site.com/iframe-url.html" /></entity>',
        data: {}
      };
    }
  }
});
registerRenderer function
render function
SYMPHONY.services.make(name, context, methods, makeEventHandlers)

Parameter

Description

name

The name of the service being created

context

The object instance, usually this

methods

The names of the methods of this object that will be available on the created service

makeEventHandlers

If true, the methods listen and fire will be added to this instance. Calling the listen or fire objects will do so on the service.

class Navigation {
    implements = ['ready', 'select'];
    serviceName = 'sample-navigation';

    register() {
        SYMPHONY.services.make(this.serviceName, this, this.implements, true);
    }

    ready() {
        this.nav = SYMPHONY.services.subscribe('applications-nav');
        this.nav.add('sample', 'My App', this.serviceName)
    }

    select() {
    // do something here
    }
}

var service = new Navigation();
service.register()
register: function(serviceName)

Parameter

Type

Description

serviceName

String

The name of the service to register

// hello:controller is a service implemented by my application
var helloControllerService = SYMPHONY.services.register("hello:controller");
subscribe : function(serviceName)

Parameter

Type

Description

serviceName

String

The name of the service to subscribe to

// modules is a service provided by the Client Extensions API
var modulesService = SYMPHONY.services.subscribe("modules");
register : function(serviceName)

Parameter

Type

Description

serviceName

String

The name of the existing local service to make available remotely

subscribe : function(serviceName)

Parameter

Type

Description

serviceName

String

The name of the remote service that you would like to access locally.

function implement(methodName, implementation)

Parameter

Type

Description

methodName

String

The name of the method to create on your service

implementation

Function

The implementation of the method

var helloAppService = SYMPHONY.services.register("hello:app");

helloAppService.implement("helloWorld", function() {
    console.log("Hello World!");
});
function implement(implementations)

Parameters

Type

Description

implementations

Object

An object containing one or more functions to create on this service, where the keys are the names of the functions and the values are the implementations

var helloAppService = SYMPHONY.services.register("hello:app");

helloAppService.implement({
    helloWorld: function() {
      console.log("Hello World!");
  }
});
function invoke(methodName, ...)

Parameters

Type

Description

methodName

String

The name of the method to call on the service

var helloAppService = SYMPHONY.services.register("hello:app");

helloAppService.implement("helloWorld", function() {
    console.log("Hello World!");
});

helloAppService.invoke("helloWorld");
function fire(eventName, ...)

Parameters

Type

Description

eventName

String

The name of the event to fire

var helloAppService = SYMPHONY.services.register("hello:app");
helloAppService.fire('myEvent');
function listen(eventName, callback)

Parameters

Type

Description

eventName

String

The name of the event to listen for

callback

Function

The function that will be called when the event is fired

var uiService = SYMPHONY.services.subscribe("ui");

// themeChangeV2 is an event fired by the UI service when the user changes his theme or font. The themeV2 object which contains the theme name and font size is also passed along with the event.
uiService.listen("themeChangeV2", function() {
  themeColor = data.themeV2.name;
  themeSize = data.themeV2.size;
  console.log("The user changed his theme color to " + themeColor " and font size to " + themeSize ".");
});
function remove(eventName, handle)

Parameters

Type

Description

eventName

String

The name of the event to unsubscribe from

handle

String

The handle returned by the call to listen

var uiService = SYMPHONY.services.subscribe("ui");

// themeChangeV2 is an event fired by the UI service when the user changes his theme or font. The themeV2 object which contains the theme name and font size is also passed along with the event.
var handle = uiService.listen("themeChangeV2", function() {
  themeColor = data.themeV2.name;
  themeSize = data.themeV2.size;
  // Styling is achieved by specifying the appropriate classes on the app module's body element.
  document.body.className = "symphony-external-app " + themeColor + " " + themeSize;
  console.log("The user changed his theme color to " + themeColor " and font size to " + themeSize ".");
});

uiService.remove("themeChangeV2", handle);
ui service
listen
// To use the modules service, you must subscribe to it from your application
var modulesService = SYMPHONY.services.subscribe("modules");
function show(id, title, serviceName, iframe, options)

Parameter

Type

Description

id

String

A unique id for this module (must be unique across all modules of a given application)

Either title or {title, icon}

String or Object

Either the title of the module as a string or an object with the keys title and icon

  • The value of title is a string

  • The value of icon is the url of a square SVG image (recommended), or the url of a square png/jpg image. Recommended size is 32x32.

serviceName

String

The name of a local service implemented by your application that will be invoked when a user action is performed relating to this module

iframe

String

The URL for the content of the module (must be an HTTPS URL)

options

Object

An object, which can contain:

  • canFloat: if set to true, a menu item will be added to the More menu (found under the (…) on the module frame) that, when clicked, will pop the module out into its own browser window

  • parentModuleId: if set to the ID of a module opened by an application, the specified module will not be closed when this module is shown

modulesService.show(
  "hello", 
  {title: "Hello World App"}, 
  "hello:controller", 
  "https://localhost:4000/app.html", 
  {
    "canFloat": true
  }
);
function hide(id)

Parameter

Type

Description

id

String

The id of the module that should be hidden.

modulesService.hide("hello");
function setTitle(id, title)

Parameters

Type

Description

id

String

The id of the module for which the title should be changed

Either title or {title, icon}

String or Object

Either the title of the module as a string or an object with the keys title and icon

  • The value of title is a string

  • The value of icon is the url of a square SVG image (recommended), or the url of a square png/jpg image. Recommended size is 32x32.

modulesService.setTitle("hello", "New Module Title");
function focus(id)

Parameter

Type

Description

id

String

The id of the module to focus

modulesService.focus("hello");
function openLink(url)

Parameter

Type

Description

url

String

The URL to be opened

// This code will live in your application view.

// Assume there is a button element with id "link" on the application module
// If that button is clicked, open a Google link.

var linkButton = document.getElementById("link");

linkButton.addEventListener("click", function(){
  modulesService.openLink("https://www.google.com");
});
function redirect(id, url)

Parameter

Type

Description

id

String

The unique identifier for the module. A module with this id must already exist.

url

String

The URL of the new iframe to load in the module.

onSelect : function(symbol) {
        this.modulesService.redirect(this.moduleId, MODULE.baseUrl + 'details?symbol=' + encodeURIComponent(symbol));
    },

Tag

Description

Attributes

<a>

Inserts a link.

• id (Optional): The tag must include either an id attribute or an href attribute. If id is specified, it must be a key to a string specifying the URL of the link. If no id attribute is specified, the href attribute is used.

<hr>

Inserts a horizontal line.

​

<icon>

Displays a 16x16 pixel icon.

• id (Required): The key to a text string specifying the URL to the image to display.

<small-icon>

Displays a 13x13 pixel icon.

• id (Required): The key to a text string specifying the URL to the image to display.

<img>

Displays a 128x128 pixel image.

• id (Optional): The tag must include either an id attribute or an src attribute. If id is specified, it must be a key to a string specifying the URL of the link. If no id attribute is specified, the src attribute is used.

<iframe>

Inserts an iframe.

• src (Required): The URL to the iframe. • height (Optional): If not specified, the default height of the iframe will be 50px. The maximum height is 1000px. • width (Optional): If not specified, the default width of the iframe will be 100%. The maximum width is 300px.

Tag

Description

Attributes

<mention>

Insert a mention.

• id (required): The key to an object with the following members: - id: The unique ID of the user being mentioned. - name: The pretty name that is displayed for the mentioned user.

<hashtag>

Inserts a hashtag.

• id (required): The key to a string specifying the hashtag. The string must be prefixed with '#'.

<cashtag>

Inserts a cashtag.

• id (required): The key to a string specifying the cashtag. The string must be prefixed with '$'.

Tag

Description

Attributes

<if>

Conditionally uses the enclosing template.

• id (required): The key to the data. If there is data at the specified key, the enclosing template is used; otherwise it is skipped.

<if:not-last>

Conditionally uses the enclosing template within an iteration. If the current iteration is not the last item in the iterated list, the enclosing template is used. This is convenient when you want to add commas between list items without adding one after the last item.

​

<if:last>

Conditionally uses the enclosing template within an iteration. If the current iteration is the last item in the iterated list, the enclosing template will be used.

​

<iterate>

Loops through the items in an array. The template between the opening and closing iterate tag is used for each array item. The data for the template references the data in the current list item.

• id (Required): The key to an array in the data object.

<entity>
    <label>Guild: </label><text id="guildName"/>
    <if id="webpageLink">
        <label>Web page: </label>
        <a id="webpageLink"><text id="webpageName"/></a>
    </if>
    <if id="people">
        <iterate id="people">
            <label>Name: </label><text id="name">
            <label>Notes:</label><br/>
            <formatted id="notes"/>
            <if:not-last><hr/></if:not-last>
        </iterate>
    </if>
</entity>
var data = {
    "content" = {
        "guildName": "Loyal Order of Water Buffalo",
        "webpageLink": "https://www.waterbuffalo.net/bedrock",
        "webpageName": "The Loyal Order of Water Buffalo, Bedrock Chapter",
        "people": [{
            "name": "Fred Flintstone",
            "notes": "Yabba Dabba Dooooo!"
        },
        {
            "name": "Barney Rubble",
            "notes": "Fred's sidekick"
        },
        {
            "name": "Dino",
            "notes": "He's kind of like a dog, but also a small sauropod. Yaps a lot.<br/> Really odd, he spoke in his first appearance."
        }]
    }
}
function xmlize(html) {
    return new XMLSerializer().serializeToString($('<span>').html(html)[0])
}
<!-- A <text> primitive must be used to inject 
     text objects from the JSON data into the template -->
<messageml>
  <a id="url"><text id="text"></a>
</messageml>
{
  url: "https://www.symphony.com",
  text: "Symphony website"
}

Tag

Description

<if>

Use this tag to conditionally use the enclosing template. This tag must include an id attribute. If there is data at the key specified by the id, then the enclosing template will be used, otherwise it will be skipped.

<if:last>

Use this tag within an iteration to conditionally use the enclosing template. If the current iteration is the last item in the iterated list, the enclosing template will be used.

<if:not-last>

Use this tag within an iteration to conditionally use the enclosing template. If the current iteration is not last item in the iterated list, the enclosing template will be used. This is useful to add commas between items in a list, without adding one to the last item.

<iterate>

Use this tag to loop through the items in an array. The template between the opening and closing <iterate> tag will be used for each item in the array. The data for the template will reference the data in current the list item. This tag must include an id attribute. This must be the key to an array in the data object.

<entity id="survey-voting-template" class="template">
    <card id="card">
        <h3>How did you like today's <text id="type"/> from <text id="venue"/>?</h3>
        <p>
            Please provide feedback by clicking one of the stars below to rate the meal.
        </p>
        <p>
            <action id="onestar"/>
            <action id="twostars"/>
            <action id="threestars"/>
            <action id="fourstars"/>
            <action id="fivestars"/>
        </p>
        <p>
            Voting ends at <text id="end"/>
        </p>
    </card>
</entity>
 onestar: {
                icon: 'icon_url',
                label: '',
                service: serviceName,
                data: {}
              }
action: function(data){
    console.log(data);
        }
// To use the commerce service, you must subscribe to it from your application
var commerceService = SYMPHONY.services.subscribe("commerce");
function getProducts(serviceName)

Parameters

Type

Description

serviceName (optional)

String

The name of a local application-implemented service. If passed, the productUpdate event will be fired on that service if the user's product subscriptions change.

{ 
  name: "<name>", 
  type: "premium", 
  sku: "<sku>", 
  subscribed: true/false
}
commerceService.getProducts('hello:controller').then(function(products) {
  console.log(products);
});
// To use the applications-nav service, you must subscribe to it from your application
var navService = SYMPHONY.services.subscribe("applications-nav");
function add(id, title, serviceName)

Parameter

Type

Description

id

String

A unique id for this navigation item (must be unique across all navigation items of a given application)

Either title or {title, icon}

String or Object

Either the title of the left navigation item as a string or an object with the keys title and icon where the value of title is a string and the value of icon is the url of a square SVG (recommended), or the url of a square png/jpg image. Recommended size is 32x32.

serviceName

String

The name of a local service implemented by your application that will be invoked when a user action is performed relating to the application navigation

// The application service that will be used to handle left navigation item clicks
var helloControllerService = SYMPHONY.services.register("hello:controller");

navService.add("hello-nav", "Hello World App", "hello:controller");

// Implement the select method on your application service
helloControllerService.implement({
  select: function(id) {
    if (id == "hello-nav") {
      console.log("hello-nav was selected.");
    }
  }
});
function remove(id)

Parameter

Type

Description

id

String

The id of the navigation item that should be removed

navService.remove('hello-nav');
function count(id, count)

Parameter

Type

Description

id

String

The id of the navigation item that should have its count updated

count

Integer

The new badge count number. Specifying 0 will hide the badge count.

navService.count("hello-nav", count);
function rename(id, title)

Parameter

Type

Description

id

String

The id of the navigation item that should be renamed

Either title or {title, icon}

String or Object

Either the title of the left navigation item as a string or an object with the keys title and icon where the value of title is a string and the value of icon is the url of a square SVG (recommended), or the url of a square png/jpg image. Recommended size is 32x32.

navService.rename('hello-nav', 'New Left Nav Title');
function focus(id)

Parameter

Type

Description

id

String

The id of the application navigation item to focus

navService.focus("hello-nav");
Application Authentication
Application Authentication
Cross-Origin Resource Sharing
ExtensionML
PresentationML
Symphony Elements
entity Service
ExtensionML
Message Format - ExtensionML
registerRenderer
render
update
pause and resume
implement
invoke
fire
listen
remove
listen
show
hide
setTitle
focus
setHandler
openLink
redirect
on-behalf-of applications

Parameter

Type

Description

template

String

An ExtensionML string that specifies the object's presentation.

In addition to ExtensionML tags, iframe tags are also supported.

data

Object

entityInstanceId

String

A unique identifier used to reference a specific entity.

Parameter

Type

Description

entityInstanceId

String

The instance id of the entity to be updated

template

String

data

Object

The data for the entity being updated

Tag

Description

Attributes

<formatted>

• id (Required): The key of a string within the template data.

<text>

Specifies regular text to be inserted into the entity. This text is inserted as is, without processing any HTML markup.

• id (Optional): The key of a string within the template data. If no id is specified, it uses the contents between the opening and closing tags.

<label>

Inserts a label. Labels are like text tags but are styled differently.

• id (Optional): The key of a string within the template data. If no id is specified, it uses the contents between the opening and closing tags.

<color-text>

Inserts a colored text. Supported colors: red, purple, green, darkGreen, blue, darkBlue, orange, grey, and yellow.

id (Optional): The key of an object with two members: • text specifies the text to be used • color is one of the listed colors.

<pill>

Inserts text with a colored background and rounded corners. Supported colors: red, purple, green, darkGreen, blue, darkBlue, orange, grey, and yellow.

id (Optional): The key of an object with two members: • text specifies the text to be used • color is one of the listed colors.

Tag

Description

<action>

OBO Authentication

OBO or On-Behalf-Of authentication allows an authenticated extension app to perform the following operations on behalf of a given user:

  • List the streams of a given user

  • Initiate connection requests to and determine connection status with other users

  • Get the presence state of other connected users

  • Initiate IMs with other users

  • Send messages and attachments

  • Set the context user's own presence

OBO use cases differ from bot use cases in that activities are performed as if end users had initiated actions directly from within Symphony themselves.

For OBO apps, authentication is a two-fold process:

  1. The app itself must be authenticated using its RSA public Key. The app authenticates only if it is enabled for the pod and its key is trusted. Upon successful OBO app authentication, the app receives an app sessionToken.

  2. The app must request to authenticate on behalf of a particular user, using its app sessionToken. The app authenticates only if it is installed for the user and its app sessionToken is valid. Upon successful OBO user authentication, the app receives the user's sessionToken.

Once the app has obtained the user's sessionToken, it can make REST API calls with this sessionToken to perform activities on behalf of the session user.

OBO App Permissions

Before proceeding, check out the OBO App permissions required for a given workflow:

Category

Permission

Description

On Behalf Of

ACT_AS_USER

Required. This required permission allows an application to act on behalf of a user via any of the other permissions. Note: This permission does not display to administrators on the Admin Portal because all apps can act on behalf of a user and therefore have the ACT_AS_USER permission by default.

Messaging

SEND_MESSAGES

The application can send messages for the logged-in user.

Messaging

SUPPRESS_MESSAGES

The application can suppress messages for the logged-in user.

Get Connections

GET_USER_CONNECTIONS

The application can get connection requests for the logged-in user.

Send Connections

REQUEST_USER_CONNECTIONS

The application can send connection requests for the logged-in user.

Get Presence

GET_PRESENCE

The application can only get presence for the logged-in user.

Set Presence

SET_PRESENCE

The application can only set presence for the logged-in user.

Primary User Identity

GET_BASIC_USER_INFO

The application can get information about the logged-in user.

Primary Contacts Access

GET_BASIC_CONTACT_INFO

The application can get information about other users through user look-up and search.

List User's Streams

LIST_USER_STREAMS

The application can list the streams in which the logged-in user is a member.

Getting Started

  1. In order to perform an OBO operation, you need to first create an extension application manifest bundle.json file and upload to the Pod.

Application Manifest Bundle File Sample:

{
  "applications": [
    {
      "type": "sandbox",
      "id": "hello",
      "name": "Hello World",
      "blurb": "This is a hello world app with a few example extension API invocations!",
      "publisher": "Symphony",
      "url": "https://localhost:4000/controller.html",
      "domain": "localhost",
      "icon": "https://localhost:4000/icon.png"
    }
  ]
}

Parameter

Type

Required

Description

type

String

Required

This field should be set to sandbox, which indicates that this is a standalone app, embedded within Symphony's client.

id

String

Required

The unique identifier for your app. Note: Do not use a colon (:) in this field, or you will receive a 401 Unauthorized error.

name

String

Required

The name of your app, which will be displayed in the Symphony Market.

blurb

String

Optional

Applied for Developer Mode. Field for display in the Symphony Market and Admin Portal.

publisher

String

Optional

The publisher of your app, which will be displayed in the Symphony Market.

url

String

Optional

URL which the pod will call to send pod information to the application

domain

String

Required

The domain for your app, which should match the controller file URL.

icon

String

Optional

An icon for your app (32x32 px), which will be displayed in the Symphony Market.

  1. Upload the manifest bundle.json to the Admin Portal -> App Management -> Add Custom App -> Import Application Bundle File

  2. Add your App Backend's (Bot) RSA public key in the Authentication section under App Management.

  3. Give your Application the following Permissions:

  4. ACT_AS_USER

Note: Give your extension application the appropriate permissions corresponding to your OBO workflow. For example, if you Bot will perform an OBO workflow to list a user's streams, grant your application with the LIST_USER_STREAMS permission.

  1. Once your App is created, make sure that it is enabled:

  2. Admin Portal -> App Settings -> Locate your App and toggle its 'Global Status' to be 'Enabled'

  3. Toggle 'Visibility' to be 'Visible'

  4. Toggle 'Installation' to be 'Manual'

  5. The last step is to make sure that the application is installed for the appropriate users. If the installation is set to 'Manual', make sure end-users install the extension application manually via the Symphony Marketplace. If not, make sure Symphony Admin installs this application on behalf of a given list of users.

Implementing OBO Authentication

The BDK makes it super simple to create an OBO based workflow, To do so, simply, simply instantiate an OBO Session in your Bot project. The BDK allows you to instantiate your OBO session from a username or user ID.

public class BotApplication {

  /** The Logger */
  private static final Logger log = LoggerFactory.getLogger(BotApplication.class);

    public static void main(String[] args) throws BdkConfigException, AuthInitializationException, AuthUnauthorizedException, Exception {

        // Initialize BDK entry point
        final SymphonyBdk bdk = new SymphonyBdk(loadFromClasspath("/config.yaml"));

        final AuthSession oboSessionUsername = bdk.obo("user.name");
        final AuthSession oboSessionUserId = bdk.obo(123456789L);

        // finally, start the datafeed read loop
        bdk.datafeed().start();
  }
}

Perform the Intended OBO workflow

In the following code snippet, the Bot authenticates on behalf of a given user and then prints a list of Streams (Type = ROOM) that the user in context is apart of:

public class BotApplication {

    private static final Logger log = LoggerFactory.getLogger(BotApplication.class);

    public static void main(String[] args) throws BdkConfigException, AuthInitializationException, AuthUnauthorizedException, Exception {

        // Initialize BDK entry point
        final SymphonyBdk bdk = new SymphonyBdk(loadFromClasspath("/config.yaml"));

        final AuthSession oboSessionUsername = bdk.obo("user.name");

        // list streams OBO user "user.name"
        List<StreamAttributes> x = bdk.streams().listStreams(oboSessionUsername, new StreamFilter());
        System.out.println(x);
        List<StreamAttributes> y = x.stream()
                .filter(item -> item.getStreamType().getType().toString().equals("ROOM"))
                .collect(Collectors.toList());
        System.out.println(y);

        // finally, start the datafeed read loop
        bdk.datafeed().start();
  }
}

Receiving Conversation and User Information

Symphony’s Extension API allows apps to extend the Symphony client user interface by adding buttons on the IM, MIM, profile, and chatroom modules. This capability can be used to build apps that act upon the room and user identity details, such as a click-to-call integration.

Prerequisites

How this works

Extension apps can receive stream participant information when an end user clicks on a button added by the app.

Your button will receive information from the user object in a MIM, IM and a room of up to 20 users.

Please note that the current user’s information isn’t returned, only the information of the other user(s) in the profile of a user, an IM, MIM or room.

Sample Objects

This is the information that you will receive if your button is pressed inside of an IM or on a users Profile:

{
   threadId,         //id of the conversation. Also known as streamId or conversationId
   userCount,        //number of users returned
   isCrossPod,       //if cross pod, returns true
   user :  {         //user information
        id,             //user identifier
        emailAddress,   //user email
        username,       //user name
          displayName,    //user display name
          firstName,      //user first name
          lastName,       //user last name
        phone,          //user phone number
        mobile,         //user mobile phone number
    }
}

You must implement the trigger() method on your application service in order to handle clicks on the registered extensions. This is a sample trigger method that will allow you to receive a user's phone number and email address as an authenticated extension app:

// The application service that willbe used to handle clicks on UI extensions
var helloControllerService = SYMPHONY.services.register("hello:controller");

// Displays a button on 1-1 instant messages
uiService.registerExtension(
  "single-user-im", 
  "hello-im", 
  "hello:controller", 
  {
    label: "IM Button", 
    data: {"datetime": Date()}
  }
);

// Implement the trigger method on your application service
helloControllerService.implement({
  trigger: function(uiClass, id, payload, data) {
    if (uiClass == "single-user-im") {
      console.log('IM button was clicked on ' + data.datetime + '.');
      // This acquires the user's phone number from the payload
      var userPhone = payload.user.phone; 
      // You can do this for any field in the prior section, such as emailAddress 
      var userEmail = payload.user.emailAddress;
      // You can now pass the user's phone number and email to your system. 
    }
  }
});

This is the information that you will receive if your button is pressed inside of a MIM or a room:

This is the information that you will receive if your button is pressed inside of a MIM or a room with less than 20 users. If there are more than 20 users, the users list will be returned empty.

{
   isCopyDisabled,   //if copy is disabled in the room returns true
   isWriteAllowed,   //if the user can send a message in the room, returns true
   isCrossPod,       //if it is a cross pod room, returns true
   roomName,         //room name
   threadId,         //id of the conversation. Also known as streamId or conversationId
   userCount,        //number of users returned
   users : [ {       //users information
       id,              //user id
       isCrossPodUser,  //if this is a cross pod user, returns true
       isOwner,         //if the user is owner of the room, returns true
       emailAddress,    //user email
       username,        //user name
       displayName,     //user display name
       firstName,       //user first name
       lastName,        //user last name
       phone,           //user phone number
       mobile,          //user mobile phone number
    }]
}
{
    isCopyDisabled,
    isWriteAllowed,
    isCrossPod,
    roomName, 
    threadId, 
    userCount
}

Build a Conversational Bot

Prerequisites

Complete the Getting Started guide:

1. Dive into the Code

BDK is a library of tools and intelligent API bindings that provides an ultra simplified configuration and authentication setup, intuitive message and room management, customizable message templating, and a new activities API that makes it easy to facilitate bot workflows. The BDK and bot project generated by the Symphony Messaging Generator makes it super easy to get started!

To begin let's open up the code generated for you by the Symphony Messaging Generator in your favorite IDE. Navigate to the BotApplication.java file:

Note:

To build a conversational workflow, add the following datafeed listener (onMessageSent) to your main bot class:

2. Next Steps

Above, we showed you a simple example of how to leverage the BDK to build a conversational bot. To see more advanced usage of the BDK in order to build complex financial workflows, continue on to our dedicated BDK Certification course:

UI Service

Use the ui service to extend various parts of the Symphony client user interface. For example, add buttons on IM (1-1 chat) and chatroom modules or add links to the #hashtag and $cashtag hovercards:

The following methods are available on the ui service:

  • openIMbyStreamID

  • openIMbyUserIDs

  • registerExtension

  • unregisterExtension

The following events are fired by the ui service:

  • themeChangeV2

openIMbyStreamID()

Open an existing conversation in a new module.

Released in version 20.10.

openIMbyUserIDs()

Open a conversation with one or more users in a new module.

Released in version 20.10.

registerExtension()

Add an action button to the Symphony user interface.

Action buttons are added to various places in the UI, such as in the header of chat modules, in #hashtag or $cashtag hovercards, on the profile of users and more.

trigger()

You must implement the trigger method on your application service in order to handle clicks on the registered action buttons:

unregisterExtension()

Remove a previously registered action button.

This will remove all instances of a particular extension - for example, from all single chat modules or all #hashtag and $cashtag hovercards.

themeChangeV2 event

This event is fired when the user's font size or theme is changed.

Circle of Trust Authentication

Application authentication establishes bidirectional trust between an application and the Symphony client.

This process must be used by enterprise or partner app developers who wants to:

  • Create a secure connection between an app and Symphony

  • Access user identity information from the app

Partner apps

Partner applications need to implement a clear process to inform and ensure that their customers have approved that users' identities will be communicated by Symphony to the partner application. Please contact the Symphony Partner team for more details.

Because apps run as iframes within the Symphony client and interact with the client via JavaScript, creating a secure connection requires interaction between an app's backend and Symphony's backend.

In the past, apps could use Client Certificates to authenticate. This is not the case anymore, and only RSA is supported.

Application Authentication Sequence

This section outlines the full app authentication sequence, covering operations that take place on the app side and the Symphony side.

Although the sequence below looks intimidating, many of the steps are completed on the Symphony side and are included for informational purposes only.

The following diagram shows the trust relationships developed through the application authentication flow.

The flow utilizes two tokens, Ta and Ts, which are generated by the app and Symphony backends after the servers mutually authenticate each other using certificates. A circle of trust is established by passing the tokens in opposite directions so that each backend server receives matching tokens via two separate paths.

The diagram below shows the interaction between the actors in the Application Authentication flow (the app frontend and backend, the Symphony frontend and backend). This flow starts after the user has logged into the Symphony client (establishing the identity of the user and trust between the Symphony client and backend). The flow begins after the user loads the partner application.

App Developer Prerequisites

Prerequisites for Authentication

You will need an RSA key pair. The public/private key pair for signing authentication requests requires the following:

• An RSA512 algorithm with a key length of 4096 bits. • An X.509 format for public keys and PKCS#1 or PKCS#8 for private keys. • PEM-encoded keys.

Generate a valid key pair by running the following generate_rsa_keys.sh script:Shell

Note: The script requires the openssl package.

Generate the PKCS#1 keys manually using the following commands:Shell

Generate the PKCS#8 keys manually using the following commands. Provide your username as the Common Name (CN).Shell

The file publickey.pem is the public key. Import this into the pod.

Sign the authentication request using either privatekey.pkcs8 or privatekey.pem, depending on the support available in the JWT library.

JSON Web Tokens

Your app will need to implement verification of the JWT shared by Symphony.

App Developer Implementation

Partner and enterprise app developers will need to implement the following sequence within their app on the frontend and backend:

Upon completion of this process, your app will know it is interacting with a valid Symphony client and can trust any user identity information it obtains from Symphony.

1. Identify the Symphony Pod

The pod ID will be useful to partner developers in building a mapping of customers to auth URL. As described in the Prerequisites section above, partners can receive the customer's auth URL through the callback.

If you are an enterprise developer building an internal custom app, you will not need to use the pod ID, since you will always use your own auth URL.

2. Initiate Backend Authentication

Once you have identified the pod where your app is being run on, you will initiate the app authentication flow. You will use the auth URL previously obtained and a JWT signed by your app RSA key. The endpoint to be used is as follows

RSA App Authenticate

POST https://<host>:<port>/login/v1/pubkey/app/authenticate/extensionApp

Authenticate your Extension App with an App Token

Request Body

In addition to specifying the JWT during this call, your app backend must generate an app token (Ta) to pass in the POST body parameters of the request. The app token must be a unique string for each authentication request. The token is opaque to Symphony.

In the response, Symphony will return back the appId, the previously presented app token, as well as a Symphony-generated token (Ts). The Symphony token is short-lived and will expire within five minutes.

Your app backend should store this token pair as they will be used for subsequent validation steps in the authentication process.

3. Register your App

You should now register your app using not just your appId, but an object containing both your appId and tokenA.

SYMPHONY.application.register will now return a promise that will be fulfilled with an object containing the members appId (your appId) and tokenS (the Symphony token).

Validate Tokens returned by Symphony Frontend

Once your app has registered, your app will need to validate the Symphony token that was passed to you through the frontend against the Symphony token you previously obtained through the Symphony backend.

The validation implementation is up to you. The implementation in the sample application provided by Symphony assumes a stateless app backend. The app frontend sends both the app and Symphony tokens back and verifies that the pair exists in the token cache. (If the partner app and server maintained a sticky session, you could pass back only the Symphony token since the app token would already have been saved in the session.)

Once the token pair match has been found, your application knows that it is interacting with a valid Symphony client.

Obtaining User Identity

User identity information can be obtained through the getJwt() method on the extended-user-info service.

getJwt()

Note that this method will only return user identity information if Steps 1-15 of the application authentication sequence have been completed (i.e. Symphony frontend trusts app frontend). If not completed, then the JWT will only contain userReferenceId, an anonymous identifier for the user.

The JWT will be returned as a base-64 encoded string, with the following format when it is decoded:

Verifying, Decoding and Using the JWT

Your app backend can validate the authenticity of the JWT returned by Symphony's frontend using the public certificate that was used to sign the JWT. The following endpoint can be used to obtain the certificate:

Using this certificate, you can verify that the JWT has not been tampered with. The signing algorithm is RS512. You should check that the algorithm specified in the header reflects this to protect against some known exploits of JWTs. The JWT can be decoded on the backend to obtain the user's identity. The decoded JWT will have the following format:

This information can then be used by to authenticate the user on the app backend.

To learn more about authenticating on behalf of a user, continue here:

FAQs

Do I need to implement application authentication?

It depends.

If you are a partner developing a premium app for Symphony and want to know user identity information, you will need to implement application authentication. For example, you may need user identity information in your app if you have entitlements or access control maintained outside of Symphony that that is tied to a user's email address.

However, if you are simply looking for a unique identifier that will allow you to map user preferences, you can do this without app authentication - by using the anonymous userReferenceId provided by Symphony.

Why isn't Symphony using OAuth or SAML?

The architecture of apps built using Symphony's Extension API poses a unique challenge. These apps run as iframes within the Symphony client and interact with the client via JavaScript. Before Symphony can share any user identity information with the app, Symphony must be certain that the app is a verified partner application. The partner application also must be certain that the user identity information is coming from Symphony, which isn't obvious with the insecure iframe model. Mechanisms like OAuth and SAML do not establish this bidirectional trust in a way that would make this possible. While getting user identity via oAuth or SAML provides a way to securely identify the user, this does not address the lack of trust between the app and Symphony frontends.

I am a partner developer building and testing my app using developer mode in the browser. Can I test this flow?

Yes, you can test app authentication using developer mode, so long as there is a certificate that has been imported and trusted on your pod that matches the appId in your bundle file.

Filter Function

You can add a new method to the service that is handling button clicks, called filter(), on any UI extension that you are implementing. Before a button is shown, that method is passed the uiClass, the id, and the payload. If filter() returns false, the button is not shown. If the method is unimplemented or it returns any value other than false, the button is shown.

Based on the information returned, you can choose to selectively display the button. For example, you can display the button only if a user's phone number is present, or if a user is not a cross-pod user.

App Authentication

Note: This guide is a conceptual overview of how Symphony performs secure app authentication. While you can implement the following workflow on your own, the BDK (Bot Developer Kit) provides an out of the box implementation of app authentication making it easy to get started building authenticated apps.

In order to create a secure connection between your app and the Symphony client, apps need to perform app authentication. Upon successful authentication, Symphony extension apps establish a bidirectional trust, allowing for secure and authorized access to user data by leveraging the Symphony Extension API.

In order to perform app authentication, your app must perform a combination of frontend and backend authentication calls to the Symphony client and pod respectively. The following steps provide an overview of the frontend and backend calls your App needs to make:

Note: Many Extension Apps' backend take the form of Symphony bots. The implementation of app authentication provided out of the box by the BDK (Bot Developer Kit) leverages this architectural design.

1. Initialize your Extension App

The first step of app authentication is a frontend call that leverages the Symphony Extension API to initialize your app. This should be used to initialize the connection to the Client Extension API from your application controller:

2. Initiate Backend Authentication

The next step of app authentication is to perform a backend authentication call to the authentication endpoint on the pod:

RSA App Authenticate

POST https://<host>:<port>/login/v1/pubkey/app/authenticate/extensionApp

Authenticate your Extension App with an App Token

Request Body

Your backend should store this token pair as they will be used for subsequent validation steps in the following authentication process.

3. Register your App

The next step of the authentication workflow is to register your app on the Symphony client using the Symphony Extension API. Registering your app requires the following frontend call to be performed in your application controller:

Response:

4. Validate Tokens returned by Symphony Frontend

The next step in the authentication workflow is to:

  • validate the App Token JWT returned from the backend API call in step 2

  • validate the Symphony JWT Token that was passed to you through the frontend against the JWT Symphony JWT Token previously attained from executing the backend API call shown in step 2.

While the implementation of this token validation is up to the developer, a sample implementation of both app token and symphony token validation is provided out of the box by the BDK. In this sample implementation, the app frontend sends both the App Token and Symphony Token to the backend where it verifies that the token pair exists in the token cache.

At this point, your app is fully authenticated and has established a bi-directional trust between itself and the Symphony client.

5. Obtain User Identity

Once you have successfully authenticated and validated your tokens, you can obtain user data through the Extension API:

The Extension API provides an extended-user-info service that contains a getJwt() method. In order to leverage this method, your app must first subscribe to the extended-user-info service:

Once subscribed, you app can leverage the getJwt() method as follows:

Deprecated Field

The username field has been changed in version 1.55.3, it now returns <email address> instead of <Symphony username>. Please note that this change has been done to help the transition for Applications that were relying on the username field and that the username field will be entirely removed in an upcoming version.

This method returns a base-64 encoded JWT token for the user in context, containing the following data when decoded:

At this point, your authenticated app has access to sensitive user data such as the Symphony user ID, username, email address, displayName, company, location, etc. Extension apps can leverage this user data in order to create user-specific workflows and automations.

Known Issue

6. OBO Authentication

If you wish to take this a step further, your app can take the JWT returned in the last step and perform authentication on behalf of (OBO) the user in context. If you wish you learn more about OBO authentication and OBO enabled workflows, continue here:

Next Steps

To learn how to properly configure and authenticate your extension app using the BDK, complete the following configuration tutorial:

Share Service

Use the share service to allow users to share content from your application into Symphony conversation:

When the share function is invoked, a modal dialog is launched and populated with the shared object content (for example, the article options). The end user can then select conversations (IMs or chatrooms) into which to share the article.

Once the article is shared, it appears in the conversation view in a card format. The article will be linked either to a webpage (if the href option is provided) or deep linked into the app (if the id option is provided).

In order to view article contents in an application (for example, if the article id is provided), the user must have the application installed.

If the recipient of a shared article does not have the application installed, the user will be prompted to install the application (provided that the user's enterprise has that application enabled).

If the recipient of a shared article does not have the application installed, and the application is not enabled for the user's enterprise, the user can view the content via the link (if href is provided). If a link is not provided, the user will be notified that the article cannot be viewed because the application is disabled for the enterprise.

The following methods are available on the share service:

share()

Launches the "Share on Symphony" modal from your application, allowing the user to share content from your application into a Symphony conversation (IM or chatroom):

The following JavaScript shows an example of an article being shared:

The following table shows the article content:

The following table shows the share options:

Sharing Third Party Content

The share function can also be used to share custom, third-party entity types. In this case, the data parameter must be populated with the following fields:

The following JavaScript shows an example of a custom third party entity being shared:

In this example, the following modal dialog is launched and populated with the shared object content:

handleLink()

You must specify your own application service for handling clicks on shared articles using handleLink if you use the id field for deep linking articles into your application.

You must implement the link method on your application service in order to handle clicks on shared articles in conversations.

Build an Interactive Bot

Prerequisites

Complete the Getting Started guide:

1. Dive into the Code

The BDK is a library of tools and intelligent API bindings that provides an ultra simplified configuration and authentication setup, intuitive message and room management, customizable message templating, and a new activities API that makes it easy to facilitate bot workflows. The BDK and bot project generated by the Symphony Messaging Generator makes it super easy to get started!

To begin let's open up the code generated for you by the Symphony Messaging Generator in your favorite IDE. Navigate to the BotApplication.java file:

Let's take a closer look at the code responsible for sending Symphony Messaging Elements in our BotApplication.java:

Bots need someway to capture the data submitted within this form. Bots can easily do so by registering a new type of Activity class:

Inside of the GifFormActivity class you will see an ActivityMatcher matcher() method:

Inside of the matcher() function, the bot is performing a validation check. If the formId of the submitted form is equal to "gif-category-form", then the bot calls the onActivity() trigger function and executes its business logic. If there is not a match, the bot does nothing and continues to listen for incoming events. As we see in our gif.ftl template above, the formId matches, so the following onActivity() trigger function executed:

2. Run your Bot

Run your bot and execute the following to see it in action:

3. Next Steps

Above, we showed you a simple example of how to leverage the BDK and Symphony Messaging Elements to build an interactive bot. To understand more advanced usage of the BDK, continue on to our dedicated BDK Certification course:

Build a Headless Bot

Prerequisites

Complete the Getting Started guide:

Spring Boot Integration

Note that for this tutorial, we've configured our bot project using the Spring Boot integration provided out of the box by the BDK. When generating your bot using the Symphony Messaging Generator, simply select 'Spring Boot' when prompted to 'Select Your Framework'.

1. Dive into the Code

The BDK is a library of tools and intelligent API bindings that provides an ultra simplified configuration and authentication setup, intuitive message and room management, customizable message templating, and a new activities API that makes it easy to facilitate bot workflows. The BDK and bot project generated by the Symphony Messaging Generator makes it super easy to get started!

To begin let's open up the code generated for you by the Symphony Messaging Generator in your favorite IDE. Navigate to the BotApplication.java file:

Notice how our BotApplication class is very simple. The idea is you have Component classes (annotated by @Component) that do the bulk of the work (see the generated GifSlashHandler , GifFormActivity, and OnUserJoinedRoomListener classes) while the main entry point is meant to really simple. The initialization of your bot project and auto-wiring of your beans (component classes) is taken care of by the BDK Spring Boot stater through the @SpringBootApplication annotation.

The following Component class, for example, is provided out of the box by the BDK and Symphony Messaging Generator:

2. Adding your own Functionality

For our headless bot workflow, we will create a RESTful Web Service that is able to handle HTTP GET/POST requests and read its JSON body payloads. Creating and bootstrapping our RESTful service is super easy since our bot application is already an integrated Spring Boot project. To get started, lets define a simple RestController class for our bot:

Here we annotate our class with the @RestControllerannotation, and use the @GetMapping annotation to map the HTTP GET request from our base path to a specific handler method, index().

Now that we have a basic RESTful service setup, let's add some Symphony Messaging specific functionality. Specifically, let's create another handler method that handles the following POST request and sends a message into a specific conversation or stream:

To create this handler method, let's add to our existing HelloController class:

  • Line 11: Inject the BDK's provided MessageService (this is the equivalent as bdk.messages())

  • Line 13-15: Initialize HelloController with an instance of MessageService.

  • Line 22-25: Create another handler method, postNotificationMessage() for a POST request to "/notification/{streamId}"

The postNotificationMessage() handler expects a streamId path parameter and also a Notification (message) as a part of the request body.

Now the only missing detail here is that we do not have the Notification Java class into which the incoming JSON will be mapped by Spring Framework's @RequestBody annotation. To do so, create the following POJO:

Now, Spring will create an instance of Notification Java class, and will set object properties with the values specified in the JSON body.

Run your bot once more and identify a streamID to use.

To easily identify a streamID, create an IM conversation with your bot, and type a message. Click on the timestamp next to the sender or recipients username and copy the streamID presented in the side panel. For this exercise you must also encode your streamID.

StreamID before encoding: n6Xl9xAZpvsJdaVPSZq8h3///omhaFQfdA==

StreamID after encoding: n6Xl9xAZpvsJdaVPSZq8h3___omhaFQfdA

Send the following POST request using either curl or postman:

You should see the following in your designated stream or chatroom:

3. Next Steps

Above, we showed you how to leverage the BDK and the Spring Boot integration to build a headless bot. To see more advanced usage of the BDK and Spring Boot, continue on to our dedicated BDK Developer Certification course:

Build an Extension App with App Views

This guide will provide an overview on how to use the Symphony Messaging App Developer Kit (ADK) to build an extension app that has app views. This app will add entries into the left navigation that will each launch a separate app view. The project will use React and ADK's React and Webpack configuration for app view generation.

Create Project

Create a working directory and initialize it using npm.

Install Dependencies

Install the Symphony Messaging ADK, the ADK React and Webpack configurations, React itself, Symphony Messaging UI Toolkit for UI Components, Typescript, Webpack and the required loaders.

Open the project directory in an editor of your choice

Add Script Commands

Edit the package.json file, replacing the scripts section with the following:

This adds two commands:

  • npm start for starting the development web server

  • npm run build to launch the production build process

Add Configuration

Create a .babelrc file with the following contents:

Create a webpack.config.js file with the following contents:

Create a tsconfig.json file with the following contents:

Create a webpack.config.js file with the following contents:

Add Application Manifest

Each extension app requires a manifest (also known as the bundle.json file) to describe the application. Create a file named bundle.json with the following contents:

Build the App

We are now ready to start building the app. Create a src directory and a file named index.js (or index.ts if you're using TypeScript) within it.

The code ADK.start() initializes the ADK with an app id (adk-example) that must correspond with the value provided in the bundle.json manifest from the previous step. Once the initialization is complete, we use ADK.navigation.add() to add an item to the left navigation bar. This item will have the label "ADK View A" and clicking on it will use ADK.modules.open() to open a module with the app view called view-a. This parameter can either be an actual navigational route (e.g. view.html) or a string that will correspond to a JavaScript or TypeScript file with the same name located in the src/views directory.

Let's proceed to build the app view itself in a file named view-a.jsx (or view-a.tsx if you're using TypeScript) within src/views.

The contents of this app view are entirely arbitrary. You can choose not to use Symphony Messaging's UI Toolkit and employ other component libraries of your choice. The only required line here is calling ADKReact.createView() at the end, passing in your component and a configuration object pointing to the same app id as before.

For aesthetics, let's define some styling in src/views/view-a.css.

Start the App

We can now start the app using:

This starts a local development server on https://localhost:4000. Note that this is a TLS-enabled site because all extension apps need to be loaded from TLS-enabled sites. However, because this is a development server, the certificate is self-signed and not trusted by any browser.

Visit https://localhost:4000 in your browser to accept the security warning about the untrusted self-signed certificate. Skipping this step will cause the extension app to not load within Symphony Messaging in the next step.

Load the App in Symphony Messaging

There are 2 ways to load an extension app into Symphony Messaging. For development purposes, we will be using the bundle injection method to temporarily load the app into the current session.

Beyond local development testing, you should get your pod administrator to create a corresponding app entry in the Admin Portal by uploading the bundle.json file.

We can now load the app by injecting the bundle URL as a parameter named bundle behind a pod URL. For example, if you are using the developer sandbox located at develop2.symphony.com, visit the following URL in your browser:

Test the App

Acknowledge the warning about being in developer mode. You should notice that a new left navigation item appears and opens an app view when clicked on.

Next Steps

Now that you have built a view-driven Extension App, you can proceed to build out your view and add more as required to complete your app.

App Developer Kit

The ADK is the modern toolkit used to build extension apps for Symphony Messaging.

Choose from one of the following guides depending on the type of extension app you require. These guides will help you get started building your extension app project from scratch.

For the complete technical reference to all methods and interfaces, please refer to the following page instead:

Bot Developer Kit for Python

Overview

The BDK for Python is Symphony Messaging's preferred tooling for Python developers to build bots. It is a library of tools and API bindings that provides simplified configuration and authentication, intuitive message and room management, customizable message templating and an activities API that makes it easy to build bot workflows.

Getting Started

Authentication

Authenticating your bot is made simple when using the BDK for Python. Once you have your bot and Symphony Messaging environment properly configured, the generated code provides an out of the box implementation for authenticating your bot:

By instantiating a new SymphonyBdk instance with your config.yaml file, the BDK loads in your config and authenticates your bot. Once authenticated, your bot is ready to leverage the REST APIs in order to create rich automations and workflows on Symphony Messaging .

OBO Authentication

BDK for Python also supports OBO (On-Behalf-Of) pattern of authentication, allowing an authenticated bot + extension application to perform operations on behalf of a given user. The BDK's implementation makes it easy to perform the following operations on behalf of a given user:

  • List the streams of a given user

  • Initiate connection requests to and determine connection status with other users

  • Get the presence state of other connected users

  • Initiate IMs with other users

  • Send messages and attachments

  • Set the context user's own presence

The guide will cover all of the prerequisites needed for OBO and how to enable & upload the OBO extension application, the required permissions and how to ensure the OBO authentication process will work successfully.

To leverage an OBO based workflow, simply instantiate an OBO Session in your bot project. The BDK allows you to instantiate your OBO session from a username or user ID. Once authenticated bots can perform any of the OBO workflows listed above:

Managing Multiple Bots

BDK for Python makes it easy to manage multiple bot instances within a single project. As long as you have unique configuration files that correspond to different service accounts, you can manage multiple bot instances from a centralized source. To do so, simply instantiate multiple bot instances of the SymphonyBDK class within your bot project:

Datafeed Management

The BDK also provides a DatafeedService interface that makes it easier than ever for bots to manage real-time messages and events. The DatafeedService interface provides the following methods for your bot to use:

For bots to listen to incoming events and messages, bots must subscribe to a custom RealTimeEventListener. This RealTimeEventListener class must implement eventType methods (e.g. onMessageSent()) along with custom business logic inside.

When a user sends a bot a message, the bot will pick up the event from the datafeed and check to see if an implemented eventType method matches the eventType (MESSAGESENT) of the inbound event. If there is a corresponding eventType method registered, the bot will execute the business logic inside of this eventType method. Otherwise the bot will not perform an action and will continue to listen for inbound events from the datafeed. An example implementation is provided out of the box by the BDK:

Below is a full list of methods provided by the RealTimeEventListener class and their corresponding eventTypes. Implement the following methods in order to listen for a given Symphony Messaging event:

For more information on the Symphony datafeed continue here:

Orchestrating Workflows with BDK for Python

A Symphony Messaging workflow can be thought of as a sequence of operations or a repeatable pattern of activities that are organized together in order to transform data, provide a service, or process information. Each of these operations or activities may be completed by a single user, shared between a bot and a user, or shared between multiple actors including bots, users, and even third party systems.

By providing an intuitive Activities API, the BDK for Python makes it simple to define a set of discrete operations or activities for different actors in your system to execute. Ultimately, these activities constitute the building blocks for a powerful Symphony Messaging workflow automation.

Once you have defined a discrete set of activities for different actors in your system to execute, the next step is to organize them together in an intelligent way.

Activities API

BDK for Python provides an Activities API, an interface that makes it easy to manage user-to-bot interactions or activities. Specifically, the Activities API provides easy access to message and room context, initiator metadata, and an intuitive way to interact with the datafeed, making it easy for bots to listen and reply to different Symphony Messaging events. The methods and logic provided by the Activities API allows for granular control over the entire user-to-bot interaction. This encapsulated logic is easily reused, forming the discrete building blocks of a Symphony Messaging workflow automation.

Registering Activities

In order to register activities for your bot instance, you must leverage the ActivityRegistry class:

There are two different types of activities supported by the BDK:

  • Command Activity: an activity triggered when a message is sent in an IM or Chatroom.

  • Form Activity: an activity triggered when a user replies to an Elements form message.

Command Activities

A command-based activity is triggered when a message is sent in an IM or Chatroom. Using the Activities API allows developers to register commands in the following formats:

  1. @bdk-bot /buy (Slash command with a bot @mention)

  1. /buy 1000 goog (Slash command without a bot @mention)

  1. Listen for the word 'hello' (Not a Slash command - Listen for a specific word)

Form Activities

The Activities API also makes it easy for Bots to listen for elements form submissions. Assume the following elements form has been posted into a room with the following attributes:

  • form id = "hello-form"

  • <text-field> name = "name"

  • form contains an action button

In order to register a form activity or listen for an incoming elements form submission, bots must register a class that extends the FormReplyActivity class:

As shown above, the Activities API makes it simple to manage incoming commands, elements form submissions, and access message context making it easy to manage bot-user interactions and create custom workflows.

User, Message & Room Management

As shown above, the BDK for Python makes it easy to create a datafeed and listen for events through the RealTimeEventListener class. In addition, this class makes it easy to access user, message, and room data in context. Each eventType method is implemented with instances of V4Initiator and V4MessageSent objects:

Use the V4Initiator class methods to access the the user data in context:

Use the V4MessageSent class methods to access message data in context:

Use the V4MessageSent class methods to access stream data in context:

Managing Context through Activities API

The Activities API also makes it easy to access relevant user, message, and stream data in context. CommandActivity classes have access to to this data through the CommandContext class. This class is instantiated with instances of V4Initiator and V4MessageSent objects. Bots are able access to the user, message, and stream data in context through the same methods shown above. Leverage these methods within the on_activity() method shown below:

FormActivity classes have access to relevant user, form, and stream data through the FormReplyContext class. This class is instantiated with instances of the V4Initiator and V4SymphonyElementsAction class. The V4SymphonyElementsAction class provides the following methods to access form data in context:

Message Templating

The corresponding Jinja template is shown below:

Using templating you can also create Element Forms. Below is an example of a price enquiry form template:

Add Buttons and Handlers to an Extension App

Prerequisites

Complete the previous guide on building an extension app with app views

Add Buttons and Handler

Let's now add a button to the hashtag hover card and a handler to link the context. Use ADK.buttons.add to add a new button to the hashtag zone. To pass the context payload from the controller to the view, there are two options: either using query parameters or using ADK to invoke an exposed controller method from a view.

Option 1: Use query parameters to pass context

In this option, we serialize the contents of the context payload and pass it directly into the ADK.modules.open call as query parameters.

Once the view is opened, you can retrieve the query parameters and deserialize it.

Option 2: Expose Method on Controller

In this option, we store the state of context on the controller, then expose a method to retrieve that state.

Once the view is opened, you can make a call to the exposed getContext method via the useRemoteExecutor hook, which returns a promise.

Symphony Messaging Generator

The Symphony Generator is a yeoman-based CLI tool that can be used to quickly generate Symphony Messaging bot, app and workflow project scaffolds. You can create example projects that use our developer toolkits:

  • BDK for Java

  • BDK for Python

  • WDK

  • ADK

Quick Start

Install yeoman and the Symphony Messaging Generator.

Then, create a directory for your new project and launch the generator.

Configure your bot, workflow or app environments accordingly.

Dialogs Service

Use the dialogs service to create modal windows (e.g. to open a modal window from a button registered at a room level).

The following methods are available on the dialogs service:

  • show

  • rerender

  • hide

The following picture is an example of what you will be able to create with this service - this module will overlay onto the entire Symphony client window:

show()

Presents a modal dialog to the user:

rerender()

Changes the contents of the dialog. This is usually invoked when the user has performed some action:

close()

An object containing the data referenced by the template. Described in .

The updated string that specifies the object presentation.

Adds text formatted with HTML markup. This must be properly formatted XML See for information on converting HTML to XHTML.

Use this tag to enable clicks on entities. For more information on how to use the action tag, refer to .

For a full list of OBO-Enabled endpoints, click .

Your app must be an Extension App.

As you can see below on line 32, we are leveraging the Activities API to register a slash command ("/gif"). While this functionality is provided out of the box, we will be building a new datafeed listener to keep it simple at first. For a more in depth look at the provided activity and Activities API, continue .

The above snippet is listening for all incoming messages that contain "/hello". To see a full list of datafeed listeners provided by the BDK, navigate . If the message received by the bot contains "/hello", the bot will respond by sending a message to the stream or conversation in context. Run your bot and let's see it in action:

As you can see, it's super easy to access the message, room, and user context from an incoming event. The V4Initiator and V4MessageSent classes provide convenience methods that make it easy to orchestrate workflows. For a more detailed overview of how to leverage the BDK's message, room, and user management classes, continue .

Extension apps can receive stream participant information when an end user clicks on a button added by the app. For more information, refer to .

Use the method on this service to subscribe to this event and specify a callback that will be executed when the event is fired. The callback should change the styling of your application to match the user's new theme.

Backend trust is established using a JWT signed by a RSA key, as you can see in the .

You should familiarize yourself with (JWT) which are the format used for passing user identity information from Symphony to your app.

You will identify the Symphony pod against which to authenticate from the existing Extension API method.JavaScript

Name
Type
Description

The method will now accept either an appId string or appData object.JavaScript

Returns identity information for the user in context in the form of a (JWT). The JWT has been signed by the private key of the pod and can be verified using the pod's public key.

Currently, calling getJwt() without having completed the application authentication sequence will return Undefined. We will be addressing this in an upcoming release so that userReferenceId will correctly be returned if application authentication has not been completed. In the interim, userReferenceId can still be obtained from the methods.

The filter function returns the same data returned by the ui Service . All data except for the the user's phone number is returned in cases where you are using an authenticated app. The user phone number is only returned for 1x1 IMs and User Profiles.

Note: Each extension app will contain a unique app id that is registered on the Pod. You can learn more about this along with other setup prerequisites in the section.

Name
Type
Description

Currently, calling getJwt() without having completed the application authentication sequence will return Undefined. We will be addressing this in an upcoming release so that userReferenceId will correctly be returned if application authentication has not been completed. In the interim, userReferenceId can still be obtained from the methods.

Here we are using the Activities API to register a new slash command that listens to "/gif". To learn more about creating your own slash commands or how to leverage the Activities API, continue . If an incoming message contains ("/gif), the bot builds a new message template which is provided out of the box for you:

The above freemarker template contains messageML that represents a Symphony Messaging Element. To learn more about Symphony Messaging Elements, continue . When a message is sent to the bot containing "/gif" the following Symphony Messaging Element is sent to the user in the conversation or stream in context:

Open up your GifFormActivity class. Here you will see that GifFormActivity extends the FormReplyActivity class. A form activity is only triggered when an end-user replies or submits an Elements form. To learn more about creating your own FormReplyActivity classes, continue .

Using the context variable, your bot can access information about the context of the form submission including the form values and the form Id. To learn more about using FormReplyActivities, continue .

To learn more about the BDK's Spring Boot Integration continue or to our dedicated BDK Developer Certification course where you will learn in depth about how to build bots using the BDK and Spring Boot:

Go ahead and start your bot application, and navigate to in a web browser of you choice. You should see the following displayed in your browser:

For more information on encoding your streamID, continue .

Prerequisite: Install NodeJS first, either or via

The BDK for Python Github repo can be found here:

The BDK for Python Certification course can be found here:

Note: You must have a corresponding service or bot account setup on your Symphony Messaging instance before authenticating. For more information navigate to the guide.

Please follow our 'Getting Started with OBO' guide using the link .

Note: If you choose to create your own CommandActivity class, you must implement the matcher() and on_activity() methods provided by the AbstractActivity class. For more information on the implementation of the CommandActivity class, continue .

Note: If you wish to create your own FormReplyActivity class, you must implement the methods matcher() and on_activity() methods provided by the AbstractActivity class. For more information on the implementation for the FormReplyActivity class, continue .

The BDK for Python also supports message templating. In order to use message templating, you must leverage the Jinja template engine. Below is an example:

Prerequisite: Install NodeJS first, either or via . You can also use other package managers like or , which the generator will attempt to use before falling back to npm.

entity advanced templating
ExtensionML
here
authenticated
here
Using Actions
package com.symphony.java;

import com.symphony.bdk.core.SymphonyBdk;
import com.symphony.bdk.core.service.datafeed.RealTimeEventListener;
import com.symphony.bdk.core.service.message.model.Message;
import com.symphony.bdk.gen.api.model.V4Initiator;
import com.symphony.bdk.gen.api.model.V4UserJoinedRoom;
import com.symphony.bdk.template.api.Template;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static com.symphony.bdk.core.config.BdkConfigLoader.loadFromClasspath;
import static com.symphony.bdk.core.activity.command.SlashCommand.slash;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonMap;

/**
 * Simple Bot Application.
 */
public class BotApplication {

  /** The Logger */
  private static final Logger log = LoggerFactory.getLogger(BotApplication.class);

  public static void main(String[] args) throws Exception {

    // Initialize BDK entry point
    final SymphonyBdk bdk = new SymphonyBdk(loadFromClasspath("/config.yaml"));

    // Register a "slash" activity
    bdk.activities().register(slash("/gif", false, context -> {
        Template template = bdk.messages().templates().newTemplateFromClasspath("/templates/gif.ftl");
        bdk.messages().send(context.getStreamId(), Message.builder().template(template).build());
    }));

    // Register a "formReply" activity that handles the Gif category form submission
    bdk.activities().register(new GifFormActivity(bdk.messages()));

    // Subscribe to 'onUserJoinedRoom' Real Time Event
    bdk.datafeed().subscribe(new RealTimeEventListener() {

      @Override
      public void onUserJoinedRoom(V4Initiator initiator, V4UserJoinedRoom event) {
        final String userDisplayName = event.getAffectedUser().getDisplayName();
        Template template = bdk.messages().templates().newTemplateFromClasspath("/templates/welcome.ftl");
        bdk.messages().send(event.getStream(),
            Message.builder().template(template, singletonMap("name", userDisplayName)).build());
      }
    });

    // finally, start the datafeed read loop
    bdk.datafeed().start();
  }
}
bdk.datafeed().subscribe(new RealTimeEventListener() {
      @Override
      public void onMessageSent(V4Initiator initiator, V4MessageSent event) {
        if (event.getMessage().getMessage().contains("/hello")){
          bdk.messages().send(event.getMessage().getStream(), String.format("<messageML>Hello %s </messageML>", initiator.getUser().getDisplayName()));
        }
      }
    });
// To use the ui service, you must subscribe to it from your application
var uiService = SYMPHONY.services.subscribe("ui");
function openIMbyStreamID(streamID, messageId)

Parameter

Type

Possible Values

Description

streamID

String

The stream ID or conversation ID to be opened.

messageID

String

Either a messageID, or the null value

The messageId can be used in addition to the streamId to focus on a specific message of the conversation. Use "null" as parameter value to jump to the latest message of the conversation.

function openIMbyUserIDs(userIds)

Parameter

Type

Possible Values

Description

userIds

String[]

Array of userIds.

function registerExtension(uiClass, id, serviceName, options)

Parameter

Type

Description

uiClass

String

The location within the Symphony application where the action button should be placed. Possible values: - single-user-im : Button added to the header of 1-1 chats - multi-user-im : Button added to the header of group chats - room : Button added to the header of chatrooms - hashtag : Link added to the hovercard that appears when hovering over a hashtag (e.g. #symphony) - cashtag : Link added to the hovercard that appears when hovering over a cashtag (e.g. $GOOG) - settings : Link added to detail card of the application in the Marketplace - profile : Link added to the user profile page and profile hovercard

id

String

A unique identifier for this extension (can be used to unregister).

serviceName

String

The name of a local service implemented by your application that will be invoked when a user clicks on your action button.

options

Object

An object, containing:

  • icon: the url of an image that will be displayed on the action button. Recommended format is a square SVG.

  • label: a label associated with the action button.

  • data: an opaque block of data that will be passed along with the trigger event

// The application service that will be used to handle clicks on UI extensions
var helloControllerService = SYMPHONY.services.register("hello:controller");
// The application service that will handle the filter on UI extensions
var helloFilterService = SYMPHONY.services.register("hello:filter");

// Displays a button in the header of 1-1 chats
uiService.registerExtension(
  "single-user-im", 
  "hello-im", 
  "hello:controller", 
  {
    label: "IM Button", 
    data: {"datetime": Date()}
  }
);

// Implement the trigger method on your application service
helloControllerService.implement({
  trigger: function(uiClass, id, payload, data) {
    if (uiClass == "single-user-im") {
      console.log('IM button was clicked on ' + data.datetime + '.');
    }
function unregisterExtension(uiClass, id)

Parameter

Type

Possible Values

Description

uiClass

String

  • single-user-im

  • multi-user-im

  • room

  • hashtag

  • cashtag

  • settings

The location within the Symphony application where the action button was registered.

id

String

The id of the UI extension that should be removed.

uiService.unregisterExtension('single-user-im', 'hello-im');
var uiService = SYMPHONY.services.subscribe("ui");

uiService.listen("themeChangeV2", function() {
  themeColor = data.themeV2.name;
  themeSize = data.themeV2.size;
  // Styling is achieved by specifying the appropriate classes on the app module's body element.
  document.body.className = "symphony-external-app " + themeColor + " " + themeSize;
});
#!/usr/bin/env bash
if [ $# -eq 0 ]
  then
    echo "Please inform a prefix for the keys. Optionally, inform as your second parameter the bit length to be used by your key generator"
    exit 1
fi
cert_prefix=$1
if [ $# -eq 2 ] 
  then
    bitlength=$2
else
    bitlength=4096
fi

mkdir $cert_prefix
cd $cert_prefix

openssl genrsa -out "${cert_prefix}_privatekey.pem" $bitlength
openssl req -newkey rsa:$bitlength -x509 -key "${cert_prefix}_privatekey.pem" -out "${cert_prefix}_publickey.cer"
openssl pkcs8 -topk8 -nocrypt -in "${cert_prefix}_privatekey.pem" -out "${cert_prefix}_privatekey.pkcs8"
openssl x509 -pubkey -noout -in "${cert_prefix}_publickey.cer"  > "${cert_prefix}_publickey.pem"

echo "Keys created in the folder: $(pwd)"
openssl genrsa -out mykey.pem 4096
openssl rsa -in mykey.pem -pubout -out pubkey.pem
openssl genrsa -out privatekey.pem 4096
openssl req -newkey rsa:4096 -x509 -key privatekey.pem -out publickey.cer
openssl pkcs8 -topk8 -nocrypt -in privatekey.pem -out privatekey.pkcs8
openssl x509 -pubkey -noout -in publickey.cer > publickey.pem
SYMPHONY.remote.hello().then(function(data){
    var pod = data.pod;
});

appToken*

String

Token generated by the application.

authToken*

String

A JWT containing the caller's username and an expiration date, signed by the caller's private key.

{
  "appId": "appId",
  "appToken": "Ta",
  "symphonyToken": "Ts",
  "expireAt": "<Unix Timestamp in milliseconds of Symphony token expiration>"
}
{
  "appId": "appId",
  "appToken": "Ta",
  "symphonyToken": "Ts",
  "expireAt": "<Unix Timestamp in milliseconds of Symphony token expiration>"
}
// appData is an object: {appId: appId, tokenA: data}

SYMPHONY.application.register(
  appData,
  ["modules", "applications-nav", "ui", "extended-user-info"],
  ["authexample:controller"]
)
  .then(validate)
  .then(function(response) {
  //...
}));
function getJwt()
{
    "aud" : "<id of app>",
    "iss" : "Symphony Communication Services LLC.",
    "sub" : "<Symphony user ID>",
    "exp" : "<expiration date in millis>",
    "user" : {
        "id" : "<Symphony user ID>",
        "emailAddress" : "<email address>",
        "username" : "<email address>",
        "firstName" : "<first name>",
        "lastName" : "<last name>",
        "displayName" : "<display name>",
        "title" : "<title>",
        "company" : "<company>",
        "companyId" : "<company (pod) ID>",
        "location" : "<location>",
        "avatarUrl" : "<URL for user's avatar>",
        "avatarSmallUrl" : "<URL for user's small avatar>"
    }
}
https://<host>/pod/v1/podcert
{
    "certificate": "<public certificate in PEM format>" // String
}
{
    "aud" : "<id of app>",
    "iss" : "Symphony Communication Services LLC.",
    "sub" : "<Symphony user ID>",
    "exp" : "<expiration date in millis>",
    "user" : {
        "id" : "<Symphony user ID>",
        "emailAddress" : "<email address>",
        "username" : "<email address>",
        "firstName" : "<first name>",
        "lastName" : "<last name>",
        "displayName" : "<display name>",
        "title" : "<title>",
        "company" : "<company>",
        "companyId" : "<company (pod) ID>",
        "location" : "<location>",
        "avatarUrl" : "<URL for user's avatar>",
        "avatarSmallUrl" : "<URL for user's small avatar>"
    }
}
// Implement the filter function on your application service
helloFilterService.implement(
   filter: function (type, id, data) {
        return !!(data.user && data.user.phone);
    }
  }
});
SYMPHONY.remote.hello()

appToken

string

Token generated by the application.

authToken

string

A JWT containing the caller's username and an expiration date, signed by the caller's private key.

{
  "appId": "appId",
  "appToken": "Ta",
  "symphonyToken": "Ts",
  "expireAt": "<Unix Timestamp in milliseconds of Symphony token expiration>"
}
SYMPHONY.application.register(appData, servicesWanted, servicesSent)

Parameter

Type

Description

appData

Object

Object containing both your appID and your app token.

servicesWanted

Array of Strings

A list of names of remote services that your application wants to subscribe to

servicesSent

Array of Strings

A list of names of local services that your application wants to make available remotely

Parameter

Type

Description

Registration Promise

Object

Object containing both your appID and Symphony JWT Token

const extendedUserInfoService = SYMPHONY.services.subscribe(
    'extended-user-info',
  );
extendedUserInfoService.getJwt()
{
    "aud" : "<id of app>",
    "iss" : "Symphony Communication Services LLC.",
    "sub" : "<Symphony user ID>",
    "exp" : "<expiration date in millis>",
    "user" : {
        "id" : "<Symphony user ID>",
        "emailAddress" : "<email address>",
        "username" : "<email address>",
        "firstName" : "<first name>",
        "lastName" : "<last name>",
        "displayName" : "<display name>",
        "title" : "<title>",
        "company" : "<company>",
        "companyId" : "<company (pod) ID>",
        "location" : "<location>",
        "avatarUrl" : "<URL for user's avatar>",
        "avatarSmallUrl" : "<URL for user's small avatar>"
    }
}
// To use the share service, you must subscribe to it from your application
var shareService = SYMPHONY.services.subscribe("share");
function share(type, content, options)

Parameter

Type

Required

Description

type

String

Yes

The type of content that is being shared.

content

Object

Yes

An object that describes the content being shared. For a list of objects see standard entities.

options

Object

No

An object that describes options that can be used to enhance the share service

// This code will live in your application view.

// Assume there is a button element with id "share" on the application module
// If that button is clicked, launch the Share modal.

var shareButton = document.getElementById("share");

var articleContent = {
  title: "Symphony Launches Mobile App",
  subTitle: "Application is mobile device management (MDM) compatible.",
  blurb: "Symphony Communication Services, a Palo Alto, Calif.-based messaging startup, announced its enterprise-ready mobile app for Apple iPhone is now available for download.",
  date : new Date("07 June 2016").getTime() / 1000,
  publisher: "Waters Technology",
  author: "Dan DeFrancesco",
  id: "symphony-article",
  thumbnail: 'https://symphony.com/example/image.png',
  href: 'https://symphony.com'
};

var shareOptions = {
  prepopulateUsers: ['71811853190920', '71811853190903']
};

// Launch Symphony's share modal when the Share button is clicked
shareButton.addEventListener("click", function(){
  shareService.share(
    "article",
    articleContent,
    shareOptions
  );
});

Field

Required

Format

Description

prepopulateUsers

No

Array of strings

The users (UserIds) who will be listed initially as recipients in the share modal.

Available only for authenticated apps, and only for Client 2.0.

It is recommended to limit the number of pre-populated users so the Symphony end user can easily review the list of recipients before sharing.

share : function(gameNbr, time)
{
    var fullTime = time;
    var hours = Math.floor(time / 60 / 60 / 1000);
    time -= hours * 60 * 60 * 1000;
    var minutes = Math.floor(time / 60 / 1000);
    time -= minutes * 60 * 1000;
    var seconds = Math.floor(time / 1000);
    var duration = hours.toString() + ':' + minutes.toString().pad(2, '0', 'left') + ':' + seconds.toString().pad(2, '0', 'left');

    var title = 'Somebody you know won at Mah Jongg Solitaire';
    var blurb = 'try to beat their time of ' + duration;
    var date = new Date().getTime() / 1000;
    var thumbnail = this.thumb;
    var id = JSON.stringify({gameNbr: gameNbr, time: fullTime});

    var presentationML =`
        <entity>
            <table><tr>
                <td><img src="${thumbnail}" /></td>
                <td>
                    <h1>${title}</h1>
                    ${blurb}
                </td>
            </tr></table>
        </entity>`;

    var entityJSON = {
        date: date,
        thumbnail: thumbnail,
        results: id,
        time : time,
        gameNbr : gameNbr,
    };

    var data = {
        plaintext: `*${title}*\n${blurb}\n`,
        presentationML : presentationML,
        entityJSON: entityJSON,
        entity: {},
        format: 'com.symphony.messageml.v2',
        inputAutofill : 'I ROCK!',
    }
    this.shareService.share('com.symfuny.invite.won', data);
}
// This code will live in your application controller.

// The application service that will be used to handle clicks on shared articles
var helloControllerService = SYMPHONY.services.register("hello:controller");

// Assume you have registered your application with the Symphony client and subscribed to the Share service.

shareService.handleLink("article", "hello:controller");

helloControllerService.implement({
    // You only need to implement this function if you intend to deeplink articles into your app by specifying an id for the article. If you use href, then article links will open in a new browser window.
  link: function(type, articleId) {
    if(type == "article") {
      // Implement this
      // For example, you might launch a new application module with a url that includes the articleId in the query parameters
      console.log("Article with id: " + articleId + " was clicked.");
    }    
  }
});
package com.symphony.java;

import com.symphony.bdk.core.SymphonyBdk;
import com.symphony.bdk.core.service.datafeed.RealTimeEventListener;
import com.symphony.bdk.core.service.message.model.Message;
import com.symphony.bdk.gen.api.model.V4Initiator;
import com.symphony.bdk.gen.api.model.V4UserJoinedRoom;
import com.symphony.bdk.template.api.Template;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static com.symphony.bdk.core.config.BdkConfigLoader.loadFromClasspath;
import static com.symphony.bdk.core.activity.command.SlashCommand.slash;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonMap;

/**
 * Simple Bot Application.
 */
public class BotApplication {

  /** The Logger */
  private static final Logger log = LoggerFactory.getLogger(BotApplication.class);

  public static void main(String[] args) throws Exception {

    // Initialize BDK entry point
    final SymphonyBdk bdk = new SymphonyBdk(loadFromClasspath("/config.yaml"));

    // Register a "slash" activity
    bdk.activities().register(slash("/gif", false, context -> {
        Template template = bdk.messages().templates().newTemplateFromClasspath("/templates/gif.ftl");
        bdk.messages().send(context.getStreamId(), Message.builder().template(template).build());
    }));

    // Register a "formReply" activity that handles the Gif category form submission
    bdk.activities().register(new GifFormActivity(bdk.messages()));

    // Subscribe to 'onUserJoinedRoom' Real Time Event
    bdk.datafeed().subscribe(new RealTimeEventListener() {

      @Override
      public void onUserJoinedRoom(V4Initiator initiator, V4UserJoinedRoom event) {
        final String userDisplayName = event.getAffectedUser().getDisplayName();
        Template template = bdk.messages().templates().newTemplateFromClasspath("/templates/welcome.ftl");
        bdk.messages().send(event.getStream(),
            Message.builder().template(template, singletonMap("name", userDisplayName)).build());
      }
    });

    // finally, start the datafeed read loop
    bdk.datafeed().start();
  }
}
// Register a "slash" activity
bdk.activities().register(slash("/gif", false, context -> {
    Template template = bdk.messages().templates().newTemplateFromClasspath("/templates/gif.ftl");
    bdk.messages().send(context.getStreamId(), Message.builder().template(template).build());
}));

// Register a "formReply" activity that handles the Gif category form submission
bdk.activities().register(new GifFormActivity(bdk.messages()));
<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 name="test" type="action">Test</button>
        <button type="reset">Reset Data</button>

    </form>
</messageML>
// Register a "formReply" activity that handles the Gif category form submission
bdk.activities().register(new GifFormActivity(bdk.messages()));
public class GifFormActivity extends FormReplyActivity<FormReplyContext> {

  private final MessageService messageService;

  public GifFormActivity(MessageService messageService) {
    this.messageService = messageService;
  }

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

  @Override
  public void onActivity(FormReplyContext context) {
    final String category = context.getFormValue("category");
    final String message = "<messageML>Received category '" + category + "'</messageML>";
    this.messageService.send(context.getSourceEvent().getStream(), Message.builder().content(message).build());
  }

  @Override
  protected ActivityInfo info() {
    return new ActivityInfo().type(ActivityType.FORM)
        .name("Gif Display category form command")
        .description("\"Form handler for the Gif Category form\"");
  }
}
@Override
  public ActivityMatcher<FormReplyContext> matcher() {
    return context -> "gif-category-form".equals(context.getFormId())
        && "submit".equals(context.getFormValue("action"));
  }
@Override
  public void onActivity(FormReplyContext context) {
    final String category = context.getFormValue("category");
    final String message = "<messageML>Received category '" + category + "'</messageML>";
    this.messageService.send(context.getSourceEvent().getStream(), Message.builder().content(message).build());
  }
package com.symphony.java;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class BotApplication {

    public static void main(String[] args) {
        SpringApplication.run(BotApplication.class, args);
    }
}
package com.symphony.java;

import com.symphony.bdk.core.activity.command.CommandContext;
import com.symphony.bdk.core.service.message.MessageService;
import com.symphony.bdk.core.service.message.model.Message;
import com.symphony.bdk.spring.annotation.Slash;
import com.symphony.bdk.template.api.Template;

import org.springframework.stereotype.Component;

@Component
public class GifSlashHandler {

  private final MessageService messageService;
  private final Template template;

  public GifSlashHandler(MessageService messageService) {
    this.messageService = messageService;
    this.template = messageService.templates().newTemplateFromClasspath("/templates/gif.ftl");
  }

  @Slash(value = "/gif", mentionBot = false)
  public void onSlashGif(CommandContext context) {
    this.messageService.send(context.getStreamId(), Message.builder().template(this.template).build());
  }
}
package com.symphony.java;

import com.symphony.bdk.gen.api.model.V4Message;
import org.springframework.web.bind.annotation.*;
import com.symphony.bdk.core.service.message.MessageService;


@RestController
public class HelloController {

    @GetMapping("/")
    public String index() {
        return "Greetings from Spring Boot!";
    }

}
curl -X POST \
  http://localhost:8080/notification/{streamId} \
  -H 'Content-Type: application/json' \
  -d '{"message":"<messageML>Hello</messageML>"}'
package com.symphony.java;

import com.symphony.bdk.gen.api.model.V4Message;
import org.springframework.web.bind.annotation.*;
import com.symphony.bdk.core.service.message.MessageService;


@RestController
public class HelloController {

    private final MessageService messageService;

    public HelloController(MessageService messageService) {
        this.messageService = messageService;
    }

    @GetMapping("/")
    public String index() {
        return "Greetings from Spring Boot!";
    }

    @PostMapping("/notification/{streamId}")
    public V4Message postNotificationMessage(@PathVariable(value="streamId") String id, @RequestBody Notification notification){
        return messageService.send(id, notification.getMessage());
    }

}
public class Notification {

    private String message;

    public String getMessage() {
        return message;
    }
}
curl -X POST \
  http://localhost:8080/notification/n6Xl9xAZpvsJdaVPSZq8h3___omhaFQfdA \
  -H 'Content-Type: application/json' \
  -d '{"message":"<messageML>hello</messageML>"}'
mkdir adk-example-views && cd $_
npm init -y
npm install \
  @symphony-ui/adk \
  @symphony-ui/adk-react \
  react \
  react-dom
npm install --save-dev \
  @symphony-ui/adk-webpack \
  @symphony-ui/uitoolkit-components \
  css-loader \
  style-loader \
  babel-loader \
  @babel/preset-react \
  webpack \
  webpack-cli \
  webpack-dev-server
npm install \
  @symphony-ui/adk \
  @symphony-ui/adk-react \
  react \
  react-dom
npm install --save-dev \
  @symphony-ui/adk-webpack \
  @symphony-ui/uitoolkit-components \
  css-loader \
  style-loader \
  ts-loader \
  typescript \
  webpack \
  webpack-cli \
  webpack-dev-server
"scripts": {
  "start": "webpack-dev-server --mode=development",
  "build": "webpack --mode=production"
},
.babelrc
{
  "presets": [ "@babel/preset-react" ]
}
webpack.config.js
const SymADKWebpack = require('@symphony-ui/adk-webpack');
const packageJson = require('./package.json');
const config = {
  devtool: 'source-map',
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        loader: "babel-loader"
      },
      {
        test: /\.css$/,
        use: [
          'style-loader',
          'css-loader',
        ],
      },
    ],
  },
  resolve: {
    extensions: ['.js', '.jsx'],
  }
};
module.exports = SymADKWebpack(config, packageJson.name);
tsconfig.json
{
  "compilerOptions": {
    "jsx": "react",
    "lib": ["ES2015", "DOM"],
  },
  "include": ["src/**/*"],
}
webpack.config.js
const SymADKWebpack = require('@symphony-ui/adk-webpack');
const packageJson = require('./package.json');
const config = {
  devtool: 'source-map',
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: 'ts-loader',
        exclude: /node_modules/,
      },
      {
        test: /\.css$/,
        use: [
          'style-loader',
          'css-loader',
        ],
      },
    ],
  },
  resolve: {
    extensions: ['.tsx', '.ts', '.js'],
  }
};
module.exports = SymADKWebpack(config, packageJson.name);
bundle.json
{
  "applications": [
    {
      "type": "sandbox",
      "id": "adk-example",
      "name": "ADK Example",
      "description": "Symphony ADK",
      "blurb": "Symphony ADK",
      "publisher": "Symphony",
      "url": "https://localhost:4000/controller.html",
      "domain": "localhost"
    }
  ]
}
src/index.js
import * as ADK from '@symphony-ui/adk';

ADK.start({ id: 'adk-example' }).then(() => {
  ADK.navigation.add('ADK View A', () => {
    ADK.modules.open('view-a', { title: 'ADK View A' });
  });
});
src/views/view-a.jsx
import * as React from 'react';
import * as ADKReact from '@symphony-ui/adk-react';
import { Badge, Icon } from '@symphony-ui/uitoolkit-components';
import { useClientTheme, useUserReferenceId } from '@symphony-ui/adk-react';
import './view-a.css';

const ViewA = () => {
  const { name: theme, layout } = useClientTheme();
  const userId = useUserReferenceId();

  return (
    <div className="main-view">
      <header>
        <h1>
          <Icon iconName="market-place" className="header-icon" />
          Welcome to ADK View A!
        </h1>
      </header>
      <main>
        <hr className='tk-my-2' />
        <h3>Meta Information</h3>
        <div>
          <strong>Theme</strong>: current theme is <Badge variant='positive'>{theme}</Badge> and <Badge variant='positive'>{layout}</Badge>
        </div>
        <div>
          <strong>User Reference Id</strong>: <Badge variant='positive'>{userId}</Badge>
        </div>
        <hr className='tk-my-2' />
      </main>
    </div>
  );
};

ADKReact.createView(<ViewA />, { id: 'adk-example' });
src/views/view-a.css
.main-view {
  font-family: "Segoe UI", Roboto, sans-serif;
  margin: 1rem;
}
.main-view header .header-icon { margin-right: 1rem }
.main-view main {
  display: flex;
  flex-direction: column;
  gap: .5rem;
}
.main-view main hr { width: 100% }
npm start
https://develop2.symphony.com/?bundle=https://localhost:4000/bundle.json
config = BdkConfigLoader.load_from_file(Path.joinpath(current_dir, 'resources', 'config.yaml'))
# setup SymphonyBdk facade object
config = BdkConfigLoader.load_from_file(Path.joinpath(current_dir, 'resources', 'config.yaml'))

# authenticate on - behalf - of a given user using username or user_id
obo_auth_session = bdk.obo(username = "username")
obo_auth_session = bdk.obo(user_id = "123456789L")

# list streams OBO user "user.name"
bdk.obo_services(obo_auth_session).streams().list_all_streams(stream_filter)
# Bot #1
config_a = BdkConfigLoader.load_from_symphony_dir("config_a.yaml")

# Bot #2
config_b = BdkConfigLoader.load_from_symphony_dir("config_b.yaml")

# use your two service accounts
async with SymphonyBdk(config_a) as bdk_a, SymphonyBdk(config_b) as bdk_b:        

Method

Descriptions

start()

Start the bot's datafeed

stop()

Stop the bot's datafeed

subscribe(RealTimeEventListener)

Subscribe a custom event listener class. Inside this class is where the bulk of your business logic goes.

unsubscribe(RealTimeEventListener)

Unsubscribe from a custom event listener class.

 # subscribe to real-time event listener
 datafeed_loop = bdk.datafeed()
 datafeed_loop.subscribe(RealTimeEventListenerImpl())
 
class RealTimeEventListenerImpl(RealTimeEventListener):

    # on a message sent, the bot replies with "Hello, {User Display Name}!"
    async def on_message_sent(self, initiator: V4Initiator, event: V4MessageSent):
       name = initiator.user.display_name
       stream_id = event.message.stream.stream_id
       response = f"Hello, {name}"
       await bdk.messages().send_message(stream_id, response)

Method

Event Type

onMessageSent()

MESSAGESENT

onInstantMessageCreated()

INSTANTMESSAGECREATED

onMessageSuppressed()

MESSAGESUPPRESSED

onRoomCreated()

ROOMCREATED

onRoomUpdated()

ROOMUPDATED

onRoomDeactivated()

ROOMDEACTIVATED

onRoomReactivated()

ROOMACTIVATED

onUserRequestedToJoinRoom()

USERREQUESTEDTOJOINROOM

onUserJoinedRoom()

USERJOINEDROOM

onUserLeftRoom()

USERLEFTROOM

onRoomMemberPromotedToOwner()

ROOMMEMBERPROMOTEDTOOWNER

onRoomMemberDemotedFromOwner()

ROOMMEMBERDEMOTEDFROMOWNER

onConnectionRequested()

CONNECTIONREQUESTED

onConnectionAccepted()

CONNECTIONACCEPTED

onSymphonyElementsAction()

SYMPHONYELEMENTSACTION

onSharedPost()

SHAREDPOST

async def run():
  async with SymphonyBdk(BdkConfigLoader.load_from_symphony_dir("config.yaml")) as bdk:
    # Access to the registry for activities
    activity_registry = bdk.activities()
async def run():
    config = BdkConfigLoader.load_from_symphony_dir("config.yaml")

    async with SymphonyBdk(config) as bdk:
        activities = bdk.activities()

        @activities.slash("/buy",                       # (1)
                          True,                         # (2)
                          "Command Description")        # (3)
        async def callback(context: CommandContext):
            logging.debug("Hello slash command triggered by user %s", context.initiator.user.display_name)
async def run():
    config = BdkConfigLoader.load_from_symphony_dir("config.yaml")

    async with SymphonyBdk(config) as bdk:
        activities = bdk.activities()

        @activities.slash("/buy {quantity} {$ticker}",  # (1)
                          False,                        # (2)
                          "Command Description")        # (3)
         async def on_echo_mention(context: CommandContext):
            # can also be retrieved with context.arguments.get("ticker").value
            ticker = context.arguments.get_cashtag("ticker").value 
            quantity = context.arguments.get_string("quantity")
            message = f"Buy ticker {ticker} with quantity {quantity}"
            # send confirmation back to user
            await messages.send_message(context.stream_id, f"{message}")
async def run():
  async with SymphonyBdk(BdkConfigLoader.load_from_symphony_dir("config.yaml")) as bdk:
    bdk.activities().register(HelloCommandActivity(bdk.messages()))
    await bdk.datafeed().start()

class HelloCommandActivity(CommandActivity):

  def __init__(self, messages: MessageService):
    self._messages = messages
    super().__init__()

  # The matches() method allows the activity logic to be triggered when a message contains hello
  def matches(self, context: CommandContext) -> bool:
     match_string = "hello"
     return context.text_content.contains(match_string)
  
  async def on_activity(self, context: CommandContext):
      # The activity logic. Here, we send a message: “Hello, There”
      await self._messages.send_message(context.stream_id, "Hello, There!)
<h2>Hello Form</h2>
<form id="hello-form">
  <text-field name="name" placeholder="Enter a name here..."/>
  <button name="submit" type="action">Submit</button>
</form>
async def run():
    async with SymphonyBdk(BdkConfigLoader.load_from_symphony_dir("config.yaml")) as bdk:
        # register ReplyFormReplyActivity Activity within the registry
        bdk.activities().register(ReplyFormReplyActivity(bdk.messages()))
        # finally, start the datafeed loop
        await bdk.datafeed().start()


class ReplyFormReplyActivity(FormReplyActivity):
    def __init__(self, messages: MessageService):
        self.messages = messages

    def matches(self, context: FormReplyContext) -> bool:
        return context.form_id == "hello-form" \ 
               and context.get_form_value("action") == "submit"

    async def on_activity(self, context: FormReplyContext):
        message = "Hello, " + context.getFormValue("name")
        await self.messages.send_message(context.source_event.stream.stream_id, message)

        
logging.config.fileConfig(Path("../logging.conf"), disable_existing_loggers=False)

try:
    logging.info("Running activity example...")
    asyncio.run(run())
except KeyboardInterrupt:
    logging.info("Ending activity example")
async def on_message_sent(self, initiator: V4Initiator, event: V4MessageSent)

Method

User Attribute

initiator.user.user_id

User ID

initiator.user.first_name

First Name

initiator.user.last_name

Last Name

initiator.user.display_name

Display Name

initiator.user.email

Email

initiator.user.user_name

Username

Method

Attribute

event.message.message_id

Message ID

event.message.timestamp

Message Timestamp

event.message.message

Message Text

event.message.shared_message

Shared Message

event.message.data

Message Data

event.message.attachments

Message Attachments

Method

Attribute

event.message.stream.stream_id

Stream ID

event.message.stream.stream_type

Stream Type

event.message.stream.room_name

Room Name

event.message.stream.members

Room Members

event.message.stream.external

External

event.message.stream.cross_pod

Cross Pod

async def on_activity(self, context: CommandContext):
    name = context.initiator.user.display_name
    await self._messages.send_message(context.stream_id, f"Hello command triggered by user {name}")

Method

Attribute

context.source_event.stream.stream_id

Elements Stream ID

context.source_event.form_message_id

Elements Message ID

context.source_event.form_id

Elements Form ID

context.source_event.form_values

Elements Form Values

async def on_activity(self, context: FormReplyContext):
    message = "Hello, " + context.get_form_value("ticker")
    await self.messages.send_message(context.source_event.stream.stream_id, message) 
from jinja2 import Template                                   #(1) Import Jinja

template = Template(open('resources/hello.jinja2').read(), autoescape=True) #(2) Load your template
message = template.render(name=user.display_name)             #(3) Construct meesage using template + data

await bdk.messages().send_message(stream_id, message)         #(4) Send templated message
Hello <b>{{ name }}</b>!
<form id="price">
    <text-field name="ticker" placeholder="Ticker" /><br />
    <button type="action" name="price">Get Price</button>
</form>
src/index.js
import * as ADK from "@symphony-ui/adk";

ADK.start({ id: "adk-example" }).then(() => {
  ADK.buttons.add("Click Me", "hashtag", (payload) => {
    console.log(`You clicked on a hashtag button`, payload);
    // Perform actions
  });
});
src/index.js
import * as ADK from "@symphony-ui/adk";

ADK.start({ id: "adk-example" }).then(() => {
  ADK.buttons.add("Click Me", "hashtag", (payload) => {
    console.log(`You clicked on a hashtag button`, payload);
    const params = "?context=" + encodeURIComponent(JSON.stringify(payload));
    ADK.modules.open("view-a" + params, { title: "ADK View A" });
  });
});
src/views/view-a.jsx
import * as React from 'react';
import * as ADKReact from '@symphony-ui/adk-react';
import { useEffect, useState } from 'react';
import './view-a.css';

const ViewA = () => {
  const [ context, setContext ] = useState();

  useEffect(() => {
    const contextString = new URLSearchParams(window.location.search).get('context');
    if (contextString) {
      setContext(JSON.parse(decodeURIComponent(contextString)));
    }
  }, []);

  return (
    <div className="main-view">
      <main>
        { context && (
          <div>
            <strong>Context</strong>: {context.entity.name}
          </div>
        )}
      </main>
    </div>
  );
};

ADKReact.createView(<ViewA />, { id: 'adk-example' });
src/index.js
import * as ADK from '@symphony-ui/adk';

ADK.start({ id: 'adk-example' }).then(() => {
  let context;
  ADK.expose({
    getContext: () => context,
  });

  ADK.buttons.add('Click Me', 'hashtag', (payload) => {
    console.log(`You clicked on a hashtag`, payload);
    context = payload;
    ADK.modules.open('view-a', { title: 'ADK View A' });
  });
});
src/index.ts
import * as ADK from '@symphony-ui/adk';

type ControllerApi = {
  getContext: () => unknown,
};

ADK.start({ id: 'adk-example' }).then(() => {
  let context;
  ADK.expose<ControllerApi>({
    getContext: () => context,
  });

  ADK.buttons.add('Click Me', 'hashtag', (payload) => {
    console.log(`You clicked on a hashtag`, payload);
    context = payload;
    ADK.modules.open('view-a', { title: 'ADK View A' });
  });
});
src/views/view-a.jsx
import * as React from 'react';
import * as ADKReact from '@symphony-ui/adk-react';
import { useRemoteExecutor } from '@symphony-ui/adk-react';
import { useEffect, useState } from 'react';
import './view-a.css';

const ViewA = () => {
  const { name: theme, layout } = useClientTheme();
  const userId = useUserReferenceId();
  const [ context, setContext ] = useState();
  const remoteExecutor = useRemoteExecutor();

  useEffect(() => {
    remoteExecutor.getContext().then((result) => setContext(result));
  }, []);

  return (
    <div className="main-view">
      <main>
        { context && (
          <div>
            <strong>Context</strong>: {context.entity.name}
          </div>
        )}
      </main>
    </div>
  );
};

ADKReact.createView(<ViewA />, { id: 'adk-example' });
src/views/view-a.tsx
import * as React from 'react';
import * as ADKReact from '@symphony-ui/adk-react';
import { useRemoteExecutor } from '@symphony-ui/adk-react';
import { useEffect, useState } from 'react';
import './view-a.css';

type ControllerApi = {
  getContext: () => Promise<unknown>,
};

const ViewA = () => {
  const [ context, setContext ] = useState();
  const remoteExecutor = useRemoteExecutor<ControllerApi>();

  useEffect(() => {
    remoteExecutor.getContext().then((result) => setContext(result));
  }, []);

  return (
    <div className="main-view">
      <main>
        { context && (
          <div>
            <strong>Context</strong>: {context.entity.name}
          </div>
        )}
      </main>
    </div>
  );
};

ADKReact.createView(<ViewA />, { id: 'adk-example' });
$ npm i -g yo @finos/generator-symphony
$ yarn global add yo @finos/generator-symphony
$ bun add -g yo @finos/generator-symphony
$ mkdir mybot && cd $_
$ yo @finos/symphony
function show(id, serviceName, template, data, options)

Parameter

Type

Description

id

String

A unique id for the dialog.

serviceName

String

The name of a local application-implemented service implemented.

template

String

The extensionML for the dialog content.

data

String

The data for the extensionML.

options

Object

The data for the extensionML

    const dialogsService = SYMPHONY.services.subscribe("dialogs");
    dialogsService.show(
        "my-dialog",
        "hello:controller",
        `<dialog>
            <div class="container">
                <div class="header">
                    <h1>Configuration</h1>
                    <br/>
                    <div class="headerError">                              
                        <text id="title"/>
                    </div>
                    <p class="value">Please check if the public and private
                    key files match and if they are correctly configured in 
                    Jira's application link section.</p>
                </div>
            </div>
        </dialog>`,
        "undefined",
        {
            title: "Application Configuration"
        }
    );
function rerender(id, template, data)

Parameter

Type

Description

id

String

The id of the dialog that should be updated.

template

String

The new extensionML content to display.

data

String

The data for the extensionML.

function close(id)

Parameters

Type

Description

id

String

The id of the dialog to close.

App Configuration
Getting Started with BDK
here
here
Developer Certification
Receiving Conversation and User Data
RSA Authentication endpoint
JSON Web Tokens
JSON web token
Register and Connect
OBO Authentication
here
Register and Connect
OBO Authentication
Getting Started with BDK
here
Developer Certification
Getting Started with BDK
Developer Certification
https://localhost:8080
Developer Certification
directly
nvm
Add Buttons and Handlers to an Extension App
Add BDK to an Extension App for Circle of Trust
Build a Basic Extension App
Build an Extension App with App Views
Build an Extension App with Message Renderers
@finos/symphony-bdk-python
learn.symphony.com/bundles/python-bot-developer
Getting Started with BDK
Creating a Bot User
here
Datafeed
here
here
Jinja
Build an Extension App with App Views
directly
nvm
bun
yarn
listen
SYMPHONY.remote.hello()
SYMPHONY.application.register
here
here
here
here
here
here

Step

Description

1

App frontend requests Symphony frontend for pod ID via the Extension API SYMPHONY.remote.hello() method.

2

Symphony frontend returns the pod ID to the app frontend.

3

App frontend sends request to its backend to initiate app backend to Symphony backend authentication, passing the pod ID.

4

App backend looks up the pod URL for backend authentication based on the pod ID. This step is only required for partners whose apps will be used by multiple customers. Enterprise developers will already know their pod URL.

5

App backend generates an app token (Ta).

6

App backend requests Symphony backend authentication using a public RSA key for the app, supplying Ta.

7

Symphony backend generates a Symphony token (Ts).

8

Symphony backend stores the (Ta, Ts) token pair for future validation.

At this point, trust has been established between the Symphony and app backends.

9

Symphony backend returns Ts to app backend.

10

App backend stores the (Ta, Ts) token pair for future validation.

11

App backend provides Ta to app frontend.

Ta is beginning the counter-clockwise journey around the the circle of trust.

12

App frontend registers app with Symphony frontend, passing appId and Ta.

Ta continues its journey.

13

Symphony frontend passes Ta to Symphony backend.

14

Symphony backend validates Ta, verifying it matches the Ta of a stored token pair.

At this point, Ta has come full circle -- the Symphony backend initially received it securely from the app backend and has now received it from the app backend again, via the app and Symphony frontends.

15

Symphony backend passes Ts back to the Symphony frontend, along with the signed user identity JWT, which will be asked for later.

At this point, the Symphony frontend trusts that the app is valid (i.e. the app server can be trusted). Because Symphony trusts the app, Symphony will be able to return protected content, like user identity information. However, the app must now trust Symphony itself in order to trust the information that is returned.

Ts is beginning the clockwise journey around the the circle of trust.

16

As a response to app registration (Step 12), Symphony frontend will return Ts.

Ts continues its journey.

17

App frontend should pass Ts to app backend for validation.

18

App backend should validate Ts against the previously stored token pair.

19

App backend should return a confirmation (or denial) to the app frontend.

At this point, the app frontend trusts that Symphony is valid and can trust the information returned by Symphony. The app can now request user identity information from Symphony and trust the response.

20

App frontend can request the user identity information by calling the getJWT() method on the extended-user-info service.

21

Symphony frontend returns the JWT which includes user identity information.

22

App frontend passes the JWT to app backend for verification.

The JWT is signed by the Symphony backend to ensure that it is not tampered with. The signature can be verified using publicly available certificate on the Symphony pod.

23

App backend requests the public app certificate used to sign the JWT from the Symphony backend.

24

Symphony backend returns the certificate in PEM format.

25

App backend verifies the JWT using the obtained certificate.

26

App backend can authenticate the user using the JWT contents. The implementation details of this are up to the developer.

27

App backend returns confirmation to app frontend.

At this point, the app fully trusts the Symphony user.

Field

Required

Format

Description

title

Yes

String

The headline of the article

subTitle

No

String

The subtitle of the article

blurb

No

String

A summary of the article to display

date

No

Date of publication

publisher

No

String

Name of the publisher

author

No

String

Name of the author

thumbnail

No

URL (could be a data url)

Image to be displayed - 106px-106px

id

Must provide either id or href, or both

String

An identifier used by the application to deeplink to the article

href

Must provide either id or href, or both

URL

URL to the article (opened in a new browser window)

Field

Description

inputAutofill

Use this to fill the comment field in the share dialog to provide initial text. Cash tags and hash tags that are specified in the text will be converted to the correct entity.

plaintext

The markdown representation of the entity, supporting a limited set of markdown features. The value of this field will be displayed on mobile devices and other older clients.

presentationML

entityJSON

The object being shared.

format

The format of the message being sent. This must be set to "com.symphony.messageml.v2".

Build a Basic Extension App

This guide will provide an overview on how to use the Symphony Messaging App Developer Kit (ADK) to build the most basic extension app. This app will add a sample entry in the left navigation bar and a button to each available target zone in the Symphony user interface. For simplicity, the project will be minimal and not use any frameworks to demonstrate the bare neccessities required to build an extension app.

Create Project

Create a working directory and initialize it using npm.

mkdir adk-example-basic && cd $_
npm init -y

Install Dependencies

Install the Symphony Messaging ADK along with the webpack bundler.

npm install @symphony-ui/adk
npm install --save-dev @symphony-ui/adk-webpack webpack-cli webpack-dev-server

Open the project directory in an editor of your choice

Add Script Commands

Edit the package.json file, replacing the scripts section with the following:

"scripts": {
  "start": "webpack-dev-server --mode=development",
  "build": "webpack --mode=production"
},

This adds two commands:

  • npm start for starting the development web server

  • npm run build to launch the production build process

Add Webpack Configuration

Create a file named webpack.config.js that will inject the ADK configuration into the webpack bundler.

webpack.config.js
const SymADKWebpack = require('@symphony-ui/adk-webpack');
const packageJson = require('./package.json');
module.exports = SymADKWebpack({}, packageJson.name);

Add Application Manifest

Each extension app requires a manifest (also known as the bundle.json file) to describe the application. Create a file named bundle.json with the following contents:

bundle.json
{
  "applications": [
    {
      "type": "sandbox",
      "id": "adk-example",
      "name": "ADK Example",
      "description": "Symphony ADK",
      "blurb": "Symphony ADK",
      "publisher": "Symphony",
      "url": "https://localhost:4000/controller.html",
      "domain": "localhost"
    }
  ]
}

Build the App

We are now ready to start building the app. Create a src directory and a file named index.js within it.

src/index.js
import * as ADK from '@symphony-ui/adk';

ADK.start({ id: 'adk-example' }).then(() => {
  ADK.navigation.add('ADK Example', () => alert('Navigate!'));

  const targets = [
    'hashtag', 'cashtag', 'single-user-im',
    'multi-user-im', 'profile', 'room'
  ];
  
  targets.forEach((target) => {
    ADK.buttons.add(
      `Button on ${target}`,
      target,
      (payload) => { console.log(`You clicked on ${target}`, payload) }
    );
  });
});

The code ADK.start() initializes the ADK with an app id (adk-example) that must correspond with the value provided in the bundle.json manifest from the previous step. Once the initialization is complete, we use ADK.navigation to add an item to the left navigation bar. This item will have the label "ADK Example" and clicking on it will pop up an alert with the content: "Navigate!" We then proceed to build an array of all the available target zones and loop through them, calling ADK.buttons to add a button into each target zone. Each button will be labelled "Button on" followed by the zone and clicking on them will log a message to the console. The message will report from which zone the button was pressed and the payload included with the event. Note that not all zones will contain a payload.

Start the App

We can now start the app using:

npm start

This starts a local development server on https://localhost:4000. Note that this is a TLS-enabled site because all extension apps need to be loaded from TLS-enabled sites. However, because this is a development server, the certificate is self-signed and not trusted by any browser.

Visit https://localhost:4000 in your browser to accept the security warning about the untrusted self-signed certificate. Skipping this step will cause the extension app to not load within Symphony Messaging in the next step.

Load the App in Symphony Messaging

There are 2 ways to load an extension app into Symphony Messaging. For development purposes, we will be using the bundle injection method to temporarily load the app into the current session.

Beyond local development testing, you should get your pod administrator to create a corresponding app entry in the Admin Portal by uploading the bundle.json file.

We can now load the app by injecting the bundle URL as a parameter named bundle behind a pod URL. For example, if you are using the developer sandbox located at develop2.symphony.com, visit the following URL in your browser:

https://develop2.symphony.com/?bundle=https://localhost:4000/bundle.json

Test the App

Acknowledge the warning about being in developer mode. You should notice that a new left navigation item appears and triggers an alert when pressed.

Next Steps

Now that you know how to build a basic extension app, you can continue to use the ADK in building out the rest of your app, depending on what type of app you require.

Integrate a Bot with an Identity Provider

Overview

By default, Symphony Messaging provides secure authentication and authorization for all users. However, if you already have an identity provider that authenticates and authorizes users for resources and you want those resources served by a bot, you will need to implement this integration yourself. This tutorial seeks to demonstrate one such implementation using an OAuth-like flow.

Components and Flow

  1. Chat Bot: Listens to commands from the user and contacts the other components

  2. Authorization Server: A server that integrates with the Identity Provider. This server is trusted with privileged access to Symphony Messaging REST APIs.

  3. Resource Server: A server where requested resources are served from. This server does not trust the bot to make arbitrary calls for user-owned data.

This sequence diagram describes the flow of requests between the various components.

Create Authorization Server

We will be using the BDK for this example, so generate a scaffold project using the Bot Generator by following these instructions. We will also be using the Spring Boot integration so ensure that is selected at the framework question in the generator.

Once you have your project, we will first add web capability by changing spring-boot-starter to spring-boot-starter-web, then adding com.auth0:java-jwt for minting tokens.

pom.xml
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>com.symphony.platformsolutions</groupId>
        <artifactId>symphony-bdk-core-spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>com.auth0</groupId>
        <artifactId>java-jwt</artifactId>
        <version>3.12.0</version>
    </dependency>
</dependencies>

Configure Authorization Server

If you haven't already configured your bot in application.yaml, do that now. Then, generate a new RSA key pair used to sign and verify tokens.

openssl genrsa -out sign-private.pem 2048
openssl rsa -in sign-private.pem -outform PEM -pubout -out sign-public.pem

Add the keys to your project and list their paths in your application.yaml file. We will also disable the BDK's datafeed since this server will initiate calls from HTTP endpoints rather than Symphony Messaging events.

application.yaml
bdk:
  host: develop2.symphony.com
  bot:
    username: bot-username
    privateKey:
      path: auth-server/rsa/auth-bot-private.pem
  datafeed:
    enabled: false

auth-server:
  privateKey: auth-server/rsa/sign-private.pem
  publicKey: auth-server/rsa/sign-public.pem

Create Message Identity Request Object

We will use this object to request for tokens from the Authorization Server, where we want to validate if a message ID is valid and the initiator's user ID matches what was requested.

MessageIdentityRequest.java
public class MessageIdentityRequest {
  private String messageId;
  private String username;
  
  // Constructors, Getters, Setters
}

Create Message Identity Service

This service will be our main logic class, serving HTTP requests and processing validation. We will inject in the location of our keys and use the BDK message service and user service.

MessageeIdentityService.java
@RestController
public class MessageIdentityService {
  @Value("${auth-server.privateKey}")
  private String privateKeyPath;

  @Value("${auth-server.publicKey}")
  private String publicKeyPath;
  
  private final MessageService messageService;
  private final UserService userService;

  public MessageIdentityService(MessageService messageService, UserService userService) {
    this.messageService = messageService;
    this.userService = userService;
  }
}

We'll then add some utilities for loading our keys from disk.

private Key readKey(String filePath, boolean isPrivate) throws Exception {
  String keyString = Files.readString(Paths.get(filePath))
    .replaceAll("-----(BEGIN|END) (PUBLIC|PRIVATE) KEY-----", "")
    .replaceAll(System.lineSeparator(), "");

  byte[] keyBytes = Base64.decodeBase64(keyString);
  KeyFactory keyFactory = KeyFactory.getInstance("RSA");

  if (isPrivate) {
    return keyFactory.generatePrivate(new PKCS8EncodedKeySpec(keyBytes));
  }
  return keyFactory.generatePublic(new X509EncodedKeySpec(keyBytes));
}

public RSAPrivateKey readPrivateKey(String filePath) throws Exception {
  return (RSAPrivateKey) readKey(filePath, true);
}

public RSAPublicKey readPublicKey(String filePath) throws Exception {
  return (RSAPublicKey) readKey(filePath, false);
}

Then we'll add a method to mint a JWT token. The contents of the token are arbitrary, but the recipient needs to understand the structure. We will be setting the user's email as the audience and the grant (or permission scope) to the subject.

private String generateJwt(String username, String subject) throws Exception {
  RSAPrivateKey privateKey = readPrivateKey(privateKeyPath);
  RSAPublicKey publicKey = readPublicKey(publicKeyPath);
  Instant now = Instant.now();
  
  return JWT.create()
    .withIssuedAt(Date.from(now))
    .withExpiresAt(Date.from(now.plus(5L, ChronoUnit.MINUTES)))
    .withAudience(username)
    .withSubject(subject)
    .sign(Algorithm.RSA512(publicKey, privateKey));
}

Next, we'll add an endpoint to serve the public key. This will be used for resource servers to validate any tokens they receive.

@GetMapping("/public-key")
public String getPublicKey() throws Exception {
  log.info("Public key retrieved");
  return Files.readString(Paths.get(publicKeyPath));
}

Finally, the core logic goes into the token request process. This method obtains the original message payload from Symphony and validates that the actual sender matches the request. It then goes ahead to extract the intent of the message - assuming that this is for command-based flows where the first non-@mention word is the intent. That word will be used as the grant. The user's email address and grant are then used to mint a JWT token and it is returned.

@PostMapping("/token")
public String getToken(@RequestBody MessageIdentityRequest request) throws Exception {
  V4Message message = messageService.getMessage(request.getMessageId());
  List<UserV2> users = userService.listUsersByEmails(List.of(request.getUsername()));

  if (!message.getUser().getUserId().equals(users.get(0).getId())) {
    throw new ResponseStatusException(BAD_REQUEST, "Message did not originate from user");
  }

  String msgText = message.getMessage()
    .replaceAll("<[^>]*>", "");
  Pattern pattern = Pattern.compile("(?<=((^|\\s)/?))(?:(?!@)[^\\s])+(?=($|\\s))");
  Matcher matcher = pattern.matcher(msgText);

  if (!matcher.find()) {
    throw new ResponseStatusException(BAD_REQUEST, "Unable to determine subject");
  }

  String username = users.get(0).getEmailAddress();
  String subject = matcher.group(0);
  return generateJwt(username, subject);
}

Create Resource Server

The resource server will be a standard Spring Boot web server, enabled with Spring Security and will have no knowledge of Symphony. Create an empty maven project and add these dependencies:

pom.xml
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.4.1</version>
    <relativePath/>
</parent>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>com.auth0</groupId>
        <artifactId>java-jwt</artifactId>
        <version>3.12.0</version>
    </dependency>
</dependencies>

Configure Resource Server

Add an application.yaml to your resources root and add in the URI to obtain the public key on the Authorization Server we added earlier. We will also change the port to 8081 so that it can run concurrently with the Authorization Server that was left defaulted to 8080.

application.yaml
server:
  port: 8081

resource-server:
  publicKeyUri: http://localhost:8080/public-key

This project will use a standard Spring Boot main class as follows.

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

Configure Spring Security

Next, add a Spring Security filter to pre-process incoming requests on the Resource Server. This filter first extracts the JWT token from the Authorization header, verifies the signature using the Authorization Server's public key, then adds the the user and grant to the security context's Authentication for use in controllers.

JWTAuthorizationFilter.java
public class JWTAuthorizationFilter extends BasicAuthenticationFilter {
  private final Logger log = LoggerFactory.getLogger(this.getClass());
  public static final String HEADER_STRING = "Authorization";
  public static final String TOKEN_PREFIX = "Bearer ";
  public final RSAPublicKey publicKey;

  public JWTAuthorizationFilter(AuthenticationManager authManager, RSAPublicKey publicKey) {
    super(authManager);
    this.publicKey = publicKey;
  }

  @Override
  protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) {
    log.info("Authorising");
    String token = req.getHeader(HEADER_STRING);

    try {
      if (token != null && token.startsWith(TOKEN_PREFIX)) {
        DecodedJWT jwt = JWT.require(Algorithm.RSA512(publicKey, null))
          .build()
          .verify(token.replace(TOKEN_PREFIX, ""));

        String subject = jwt.getSubject();
        String audience = jwt.getAudience().get(0);
        
        // In reality: check that this user exists

        log.info("Authorised: [{}] {}", audience, subject);

        SecurityContextHolder.getContext().setAuthentication(
          new UsernamePasswordAuthenticationToken(audience, subject, List.of()));
      }
      chain.doFilter(req, res);
    } catch (Exception e) {
      log.error("Authorisation failed");
    }
  }
}

This filter then needs to be added to the Spring Security configuration. Before that happens, the public key is fetched from the Authorization Server and loaded.

SecurityConfig.java
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
  @Value("${resource-server.publicKeyUri}")
  private String publicKeyUri;

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    // Fetch public key from Authorization Server
    HttpRequest request = HttpRequest.newBuilder()
      .uri(new URI(publicKeyUri)).build();

    String response = HttpClient.newBuilder()
      .build().send(request, HttpResponse.BodyHandlers.ofString()).body();
    
    // Load public key
    String keyString = response
      .replaceAll("-----(BEGIN|END) PUBLIC KEY-----", "")
      .replaceAll(System.lineSeparator(), "");

    byte[] keyBytes = Base64.decodeBase64(keyString);
    KeyFactory keyFactory = KeyFactory.getInstance("RSA");
    RSAPublicKey publicKey = (RSAPublicKey) keyFactory.generatePublic(new X509EncodedKeySpec(keyBytes));
    
    // Configure all endpoints to be authenticated
    // and use this auth filter in the chain
    http.authorizeRequests()
      .anyRequest().authenticated()
      .and()
      .addFilter(new JWTAuthorizationFilter(authenticationManager(), publicKey));
  }
}

Add Resource Endpoint

Finally, the actual resource is served on a standard controller. This controller does a further check against the grant to ensure it aligns with the resource being requested.

BalanceController.java
@RestController
public class BalanceController {
  @GetMapping("/api/balance")
  public long getBalance(UsernamePasswordAuthenticationToken token) {
    String grant = token.getCredentials().toString();
    if (grant.equals("/balance")) {
      // in reality: fetch the user's actual balance
      return (long) (Math.random() * 777);
    }
    else {
      throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Incorrect grant");
    }
  }
}

Create Bot

We are now ready to build the bot itself. This project is similar to the Authorization Server in that it uses BDK 2.0 with the Spring Boot integration, so repeat that process once more. We will add the openfeign project for calling our HTTP endpoints.

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>com.symphony.platformsolutions</groupId>
        <artifactId>symphony-bdk-core-spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
        <version>3.0.0</version>
    </dependency>
</dependencies>

We will need to enable feign on the main class by adding the @EnableFeignClients annotation.

Bot.java
@SpringBootApplication
@EnableFeignClients
public class Bot {
  public static void main(String[] args) {
    SpringApplication.run(Bot.class, args);
  }
}

Configure Bot

Add the bot configuration itself and then the location of our Authorization and Resource servers.

application.yaml
bdk:
  host: develop2.symphony.com
  bot:
    username: bot-username
    privateKey:
      path: bot/rsa/user-bot-private.pem

bot:
  auth-server-uri: http://localhost:8080
  resource-server-uri: http://localhost:8081

Add HTTP Clients

MessageIdentityClient.java
@FeignClient(name = "MessageIdentityClient", url = "${bot.auth-server-uri}")
public interface MessageIdentityClient {
  @PostMapping("/token")
  String getToken(MessageIdentityRequest request);
}
BalanceClient.java
@FeignClient(name = "BalanceClient", url = "${bot.resource-server-uri}")
public interface BalanceClient {
  @GetMapping("/api/balance")
  String getBalance(@RequestHeader("Authorization") String token);
}

Add Command Handler

Finally, we'll add a command handler to receive a /balance command from the user. This will first request for a token from the Authorization Server, then use that token to request for the user's bank balance before returning that value in a message to the user.

BalanceCommandHandler.java
@Component
public class BalanceCommandHandler {
  private final Logger log = LoggerFactory.getLogger(this.getClass());
  private final MessageService messageService;
  private final MessageIdentityClient messageIdentityClient;
  private final BalanceClient balanceClient;

  public BalanceCommandHandler(
    MessageService messageService,
    MessageIdentityClient messageIdentityClient,
    BalanceClient balanceClient
  ) {
    this.messageService = messageService;
    this.messageIdentityClient = messageIdentityClient;
    this.balanceClient = balanceClient;
  }

  @Slash(value = "/balance", mentionBot = false)
  public void showBalance(CommandContext context) {
    log.info("Balance requested");
    String messageId = context.getMessageId();
    String username = context.getInitiator().getUser().getEmail();

    try {
      // Get token from auth server
      MessageIdentityRequest request = new MessageIdentityRequest(messageId, username);
      String token = messageIdentityClient.getToken(request);

      // Fetch data from resource server
      String authToken = "Bearer " + token;
      String balance = balanceClient.getBalance(authToken);
      response = "Your balance is: $" + balance;
    } catch (Exception e) {
      response = "You are unauthorised to use this feature";
    }

    // Respond to user
    messageService.send(context.getStreamId(), response);
  }
}

Result

Launch your project now and go to your Symphony pod, search for your bot and issue a /balance command. If your current user account is authorized by both the Authorization and Resource Servers, you will see an account balance message appear like on the left pane below. If not, you will observe the message on the right pane instead.

Source Code

Bot Developer Kit for Java

Overview

The BDK for Java is Symphony Messaging's preferred tooling for Java developers to build bots. It is a library of tools and API bindings that provides simplified configuration and authentication, intuitive message and room management, customizable message templating and an activities API that makes it easy to build bot workflows.

Getting Started

Authentication

Once you have your bot and Symphony Messaging environment properly configured, the generated code provides an out-of-the-box implementation for authenticating your bot:

SymphonyBdk bdk = new SymphonyBdk(loadFromClasspath("/config.yaml"));

By instantiating a new SymphonyBdk instance with your config.yaml file, the BDK loads in your config and authenticates your bot. Once authenticated, your bot is ready to leverage the REST APIs in order to create rich automations and workflows on Symphony Messaging.

OBO Authentication

BDK for Java also supports the OBO (On-Behalf-Of) pattern of authentication, allowing an authenticated bot + extension application to perform operations on behalf of a given user. The BDK's implementation makes it easy to perform the following operations on behalf of a given user:

  • List the streams of a given user

  • Initiate connection requests to and determine connection status with other users

  • Get the presence state of other connected users

  • Initiate IMs with other users

  • Send messages and attachments

  • Set the context user's own presence

To leverage an OBO based workflow, simply instantiate an OBO Session in your bot project. The BDK allows you to instantiate your OBO session from a username or user ID. Once authenticated bots can perform any of the OBO workflows listed above:

 // setup SymphonyBdk facade object
 final SymphonyBdk bdk = new SymphonyBdk(loadFromSymphonyDir("config.yaml"));

 //authenticate on-behalf-of a given user
 final AuthSession oboSessionUsername = bdk.obo("user.name");
 final AuthSession oboSessionUserId = bdk.obo(123456789L);

 // list streams OBO user "user.name"
 bdk.streams().listStreams(oboSessionUsername, new StreamFilter());

Managing Multiple Bots

BDK for Java makes it easy to manage multiple bot instances within a single project. As long as you have unique configuration files that correspond to different service accounts, you can manage multiple bot instances from a centralized source. To do so, simply instantiate multiple bot instances of the SymphonyBDK class within your bot project:

// Bot #1
final SymphonyBdk bot1 = new SymphonyBdk(loadFromClasspath("/config_1.yaml"));

//Bot #2
final SymphonyBdk bot2 = new SymphonyBdk(loadFromClasspath("/config_2.yaml"));

Datafeed Management

The BDK also provides a DatafeedService interface that makes it easier than ever for bots to manage real-time messages and events. The DatafeedService interface provides the following methods for your bot to use:

Method

Descriptions

start()

Start the bot's datafeed

stop()

Stop the bot's datafeed

subscribe(RealTimeEventListener)

Subscribe a custom event listener class. Inside this class is where the bulk of your business logic goes.

unsubscribe(RealTimeEventListener)

Unsubscribe from a custom event listener class.

For bots to listen to incoming events and messages, bots must subscribe to a custom RealTimeEventListener. This RealTimeEventListener class must implement eventType methods (e.g. onMessageSent()) along with custom business logic inside.

When a user sends a bot a message, the bot will pick up the event from the datafeed and check to see if an implemented eventType method matches the eventType (MESSAGESENT) of the inbound event. If there is a corresponding eventType method registered, the bot will execute the business logic inside of this eventType method. Otherwise the bot will not perform an action and will continue to listen for inbound events from the datafeed. An example implementation is provided out of the box by the BDK:

 // subscribe to "onMessageSent" real-time event
    bdk.datafeed().subscribe(new RealTimeEventListener() {

      @Override
      public void onMessageSent(V4Initiator initiator, V4MessageSent event) {
        // on a message sent, the bot replies with "Hello, {User Display Name}!"
        bdk.messages().send(event.getMessage().getStream(), "<messageML>Hello, " + initiator.getUser().getDisplayName() + "!</messageML>");
      }
    });

Below is a full list of methods provided by the RealTimeEventListener class and their corresponding eventTypes. Implement the following methods in order to listen for a given Symphony Messaging event:

Method

Event Type

onMessageSent()

MESSAGESENT

onInstantMessageCreated()

INSTANTMESSAGECREATED

onMessageSuppressed()

MESSAGESUPPRESSED

onRoomCreated()

ROOMCREATED

onRoomUpdated()

ROOMUPDATED

onRoomDeactivated()

ROOMDEACTIVATED

onRoomReactivated()

ROOMACTIVATED

onUserRequestedToJoinRoom()

USERREQUESTEDTOJOINROOM

onUserJoinedRoom()

USERJOINEDROOM

onUserLeftRoom()

USERLEFTROOM

onRoomMemberPromotedToOwner()

ROOMMEMBERPROMOTEDTOOWNER

onRoomMemberDemotedFromOwner()

ROOMMEMBERDEMOTEDFROMOWNER

onConnectionRequested()

CONNECTIONREQUESTED

onConnectionAccepted()

CONNECTIONACCEPTED

onSymphonyElementsAction()

SYMPHONYELEMENTSACTION

onSharedPost()

SHAREDPOST

For more information on the Symphony Messaging datafeed continue here:

Orchestrating Workflows with BDK for Java

A Symphony Messaging workflow can be thought of as a sequence of operations or a repeatable pattern of activities that are organized together in order to transform data, provide a service, or process information. Each of these operations or activities may be completed by a single user, shared between a bot and a user, or shared between multiple actors including bots, users, and even third party systems.

By providing an intuitive Activities API, the BDK makes it simple to define a set of discrete operations or activities for different actors in your system to execute. Ultimately, these activities constitute the building blocks for a powerful Symphony Messaging workflow automation.

Once you have defined a discrete set of activities for different actors in your system to execute, the next step is to organize them together in an intelligent way. The BDK provides a powerful Workflow API (coming soon) that makes it easy to organize a sequence of activities together, and subsequently orchestrate a Symphony Messaging workflow.

Activities API

BDK for Java provides an Activities API, an interface that makes it easy to manage user-to-bot interactions or activities. Specifically, the Activities API provides easy access to message and room context, initiator metadata, and an intuitive way to interact with the datafeed, making it easy for bots to listen and reply to different Symphony Messaging events. The methods and logic provided by the Activities API allows for granular control over the entire user-to-bot interaction. This encapsulated logic is easily reused, forming the discrete building blocks of a Symphony Messaging workflow automation.

Registering Activities

In order to register activities for your bot instance, you must leverage the ActivityRegistry class:

public static void main(String[] args) throws Exception {
    // Create BDK entry point
    final SymphonyBdk bdk = new SymphonyBdk(loadFromSymphonyDir("config.yaml"));
    // Access to the registry for activities
    final ActivityRegistry registry = bdk.activities();
  }

There are two different types of activities supported by the BDK:

  • Command Activity: an activity triggered when a message is sent in an IM or Chatroom.

  • Form Activity: an activity triggered when a user replies to an Elements form message.

Command Activities

A command-based activity is triggered when a message is sent in an IM or Chatroom. Using the Activities API allows developers to register commands in the following formats:

  • @bot/buy (Slash command with a bot @mention)

bdk.activities().register(slash("/hello", true, context -> {
  String message = "Hello " + context.getInitiator().getUser().getDisplayName();
  bdk.messages().send(context.getStreamId(), message);
}));
  • /buy 1000 $goog (Slash command without a bot @mention)

bdk.activities().register(slash("/buy {quantity} {$ticker}", false, context -> {
  Arguments arguments = context.getArguments();
  String quantity = arguments.getString("quantity");
  String ticker = arguments.getAsString("ticker").substring(1);
  String message = "Buy " + quantity + " of <cash tag=\"" + ticker + "\" />";
  bdk.messages().send(context.getStreamId(), message);
}));
  • Listen for the word hello (Not a Slash command - just listen for messages containing a specific word)

bdk.activities().register(new CommandActivity<>() {
  protected ActivityMatcher<CommandContext> matcher() throws EventException {
    return c -> c.getTextContent().contains("hello");
  }
  
  protected void onActivity(CommandContext context) throws EventException {
    bdk.messages().send(context.getStreamId(), "Hello!");
  }
  
  protected ActivityInfo info() {
    return new ActivityInfo().name("Hello Command");
  }
});

Form Activities

The Activities API also makes it easy for Bots to listen for elements form submissions. Assume the following elements form has been posted into a room with the following attributes:

  • form id = "hello-form"

  • <text-field> name = "name"

  • form contains an action button

<h2>Hello Form</h2>
<form id="hello-form">
  <text-field name="name" placeholder="Enter a name here..." />
  <button name="submit" type="action">Submit</button>
</form>

In order to register a form activity or listen for an incoming elements form submission, bots must register a class that extends the FormReplyActivity class:

bdk.activities().register(new FormReplyActivity<>() {
  protected ActivityMatcher<FormReplyContext> matcher() throws EventException {
    return c -> c.getFormId().equals("hello-form");
  }

  protected void onActivity(FormReplyContext context) throws EventException {
    final String message = "Hello, " + context.getFormValue("name");
    bdk.messages().send(context.getStreamId(), message);
  }

  protected ActivityInfo info() {
    return new ActivityInfo().name("Hello Form");
  }
});

As shown above, the Activities API makes it simple to manage incoming commands, elements form submissions, and access message context making it easy to manage bot-user interactions and create custom workflows.

User, Message & Room Management

As shown above, the BDK makes it easy to create a datafeed and listen for events through the RealTimeEventListener class. In addition, this class makes it easy to access user, message, and room data in context. Each eventType method is implemented with instances of V4Initiator and V4MessageSent objects:

public void onMessageSent(V4Initiator initiator, V4MessageSent event)

Use the V4Initiator class methods to access the the user data in context:

Method

User Attribute

initiator.getUser().getUserId()

User ID

initiator.getUser().getFirstName()

First Name

initiator.getUser().getLastName()

Last Name

initiator.getUser().getDisplayName()

Display Name

initiator.getUser().getEmail()

Email

initiator.getUser().getUsername()

Username

Use the V4MessageSent class methods to access message data in context:

Method

Attribute

event.getMessage().getMessageId()

Message ID

event.getMessage().getTimestamp()

Message Timestamp

event.getMessage().getMessage()

Message Text

event.getMessage().getSharedMessage()

Shared Message

event.getMessage().getData()

Message Data

event.getMessage().getAttachments()

Message Attachments

Use the V4MessageSent class methods to access stream data in context:

Method

Attribute

event.getMessage().getStream().getStreamId()

Stream ID

event.getMessage().getStream().getStreamType()

Stream Type

event.getMessage().getStream().getRoomName()

Room Name

event.getMessage().getStream().getMembers()

Room Members

event.getMessage().getStream().getExternal()

External

event.getMessage().getStream().getCrossPod()

Cross Pod

Managing Context through Activities API

The Activities API also makes it easy to access relevant user, message, and stream data in context. CommandActivity classes have access to to this data through the CommandContext class. This class is instantiated with instances of V4Initiator and V4MessageSent objects. Bots are able access to the user, message, and stream data in context through the same methods shown above. Leverage these methods within the onActivity() method shown below:

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

FormActivity classes have access to relevant user, form, and stream data through the FormReplyContext class. This class is instantiated with instances of the V4Initiator and V4SymphonyElementsAction class. The V4SymphonyElementsAction class provides the following methods to access form data in context:

Method

Attribute

context.getSourceEvent().getStream()

Elements Stream ID

context.getSourceEvent().getFormMessageId()

Elements Message ID

context.getSourceEvent().getFormId()

Elements Form ID

context.getSourceEvent().getFormValues()

Elements Form Values

@Override
  protected void onActivity(FormReplyContext context) {
    final String message = "You entered " + context.SourceEvnent().getFormValues() + ".";
    this.messageService.send(context.getSourceEvent().getStream(), "<messageML>" + message + "</messageML>");
  }

Message Templating

The BDK for Java also supports custom and built in message templating. The BDK is agnostic to what templating library developers choose, with built-in support for FreeMarker and Handlebars. In order to use message templating, you must leverage the TemplateEngine class provided by the BDK.

String message = bdk.messages().templates()
    .newTemplateFromString("Hello ${value}")
    .process(Map.of("value", "World"));
bdk.messages().send(context.getStreamId(), message);

If you wish to build your own custom message template, you must implement one of the newTemplate() methods provided by the TemplateEngine class:

  • newTemplateFromFile()

  • newTemplateFromClasspath()

  • newTemplateFromString()

The following shows an implementation of the newTemplateFromClasspath() method:

String message = bdk.messages().templates()
    .newTemplateFromClasspath("hello.ftl")
    .process(Map.of("value", "World"));
bdk.messages().send(context.getStreamId(), message);

The corresponding FreeMarker template should be stored in the classpath root i.e. src/main/resources/hello.ftl

Hello ${value}

Spring Boot Integration

The BDK for Java's Spring Boot integration provides native annotations, making it easy to configure your bot's datafeed listeners and register command activities. Start with a standard Spring Boot main class.

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

@Component

Now you can create a component for bot applications by annotating classes with the @Component annotation:

@Component
public class HelloCommand {
  @Autowired
  private MessageService messages;

  @EventListener
  public void onMessageSent(RealTimeEvent<? extends V4MessageSent> event) {
    messages.send(event.getSource().getMessage().getStream(), "Hello!");
  }
}

@EventListener

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

  • Have a com.symphony.bdk.spring.events.RealTimeEvent<? extends T> parameter

@Slf4j
@Component
public class RealTimeEventsDemo {
  @EventListener
  public void onMessageSent(RealTimeEvent<? extends V4MessageSent> event) {
    log.info(event.toString());
  }

  @EventListener
  public void onUserJoined(RealTimeEvent<? extends V4UserJoinedRoom> event) {
    log.info(event.toString());
  }

  @EventListener
  public void onUserLeft(RealTimeEvent<? extends V4UserLeftRoom> event) {
    log.info(event.toString());
  }
}

@Slash

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:

@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)");
  }
}

Activities

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

@Slf4j
@Component
public class GifFormActivity extends FormReplyActivity<FormReplyContext> {
  @Autowired
  private MessageService messageService;

  @Slash("/gif")
  public void displayGifForm(CommandContext context) throws TemplateException {
    this.messageService.send(context.getStreamId(), "/templates/gif.ftl", emptyMap());
  }
  
  public ActivityMatcher<FormReplyContext> matcher() {
    return context -> "gif-category-form".equals(context.getFormId())
        && "submit".equals(context.getFormValue("action"))
        && StringUtils.isNotEmpty(context.getFormValue("category"));
  }
  
  public void onActivity(FormReplyContext context) {
    log.info("Gif category is \"{}\"", context.getFormValue("category"));
  }

  protected ActivityInfo info() {
    return new ActivityInfo().type(ActivityType.FORM)
        .name("Gif Display category form command")
        .description("Form handler for the Gif Category form");
  }
}
Figure 1: Circle of Trust
Figure 2: Trust Sequence

For more details on the JWT, see the section on .

For more details on this, see the section on .

For more details on the JWT, see the section on .

The default presentation of the entity using . This will be seen by everybody who does not have an app with a custom renderer for the given type.

Prerequisite: Install NodeJS first, either or via

Copy the class from the Authorization Server project into this project. Then, create 2 feign clients to communicate with the Authorization and Resource Server endpoints.

The complete project source code for this tutorial can be found at the following repository.

has been released! The BDK 2.0 will stop being supported in August 2024. Please consider migrating to keep up with the latest features!

The BDK for Java Github repo can be found here:

The BDK for Java Certification course can be found here:

Note: You must have a corresponding service or bot account setup on your Symphony Messaging instance before authenticating. For more information navigate to the guide.

Please follow our 'Getting Started with OBO' guide using the link . The guide will cover all of the prerequisites needed for OBO and how to enable & upload the OBO extension application, the required permissions and how to ensure the OBO authentication process will work successfully.

Note: If you choose to create your own CommandActivity class, you must implement the matcher() and onActivity() methods provided by the AbstractActivity class. For more information on the implementation of the CommandActivity class, continue .

Note: If you wish to create your own FormReplyActivity class, you must implement the methods matcher(), onActivity() and info() methods provided by the AbstractActivity class. For more information on the implementation for the FormReplyActivity class, continue .

The Core Starter uses to deliver Real Time Events.

Be annotated with

Unix Epoch Timestamp
presentationML
directly
nvm
Build an Extension App with App Views
Build an Extension App with Message Renderers
Getting Started with BDK
https://github.com/SymphonyPlatformSolutions/symphony-sso-example
BDK 3.0
@finos/symphony-bdk-java
learn.symphony.com/bundles/java-bot-developer
Getting Started with BDK
Creating a Bot User
here
Datafeed
here
here
Spring Events
@EventListener
Obtaining User Identity
Validating Tokens
Obtaining User Identity
MessageIdentityRequest
Symphony Application Developer Kit