GitHub - unblu/unblu-middleware-lib: This library is used to create a middleware for the Unblu v8 platform (API v4), based on Spring Boot. It provides a set of tools and utilities to facilitate the integration with Unblu services.

This library is used to create middleware for the Unblu v8 platform (API v4). It provides integrations for Spring Boot and Quarkus.

NOTE: The legacy aggregate artifacts (unblu-middleware-lib-core, unblu-middleware-lib-spring, unblu-middleware-lib-quarkus) were removed. Use feature modules instead.

Modules

Recommended:

  • com.unblu.middleware:unblu-middleware-lib-bom - dependency version alignment for all feature modules

Core feature modules (framework-agnostic):

  • unblu-middleware-lib-core-common

  • unblu-middleware-lib-core-webhooks

  • unblu-middleware-lib-core-outboundrequests

  • unblu-middleware-lib-core-dialog-bot

  • unblu-middleware-lib-core-conversation-observing-bot

  • unblu-middleware-lib-core-external-messenger

Spring Boot feature modules:

  • unblu-middleware-lib-spring-webhooks

  • unblu-middleware-lib-spring-outboundrequests

  • unblu-middleware-lib-spring-dialog-bot

  • unblu-middleware-lib-spring-conversation-observing-bot

  • unblu-middleware-lib-spring-external-messenger

Quarkus feature modules:

  • unblu-middleware-lib-quarkus-webhooks

  • unblu-middleware-lib-quarkus-outboundrequests

  • unblu-middleware-lib-quarkus-dialog-bot

  • unblu-middleware-lib-quarkus-conversation-observing-bot

  • unblu-middleware-lib-quarkus-external-messenger

Main features:

  • Provides beans for Unblu API clients (conversationApi, webhooksApi, botsApi, etc.)

  • Automatically creates, manages, and heals bot/webhook registrations

  • Handles registered webhooks, bot outbound requests, and pings

  • Supports synchronous handlers and reactive handlers (Mono) with order/context options

Quarkus feature modules

In Quarkus applications, features are enabled by classpath. Add only the modules you need:

  • unblu-middleware-lib-quarkus-webhooks

  • unblu-middleware-lib-quarkus-outboundrequests

  • unblu-middleware-lib-quarkus-dialog-bot

  • unblu-middleware-lib-quarkus-conversation-observing-bot

  • unblu-middleware-lib-quarkus-external-messenger

Quick start - webhooks (Spring Boot)

Add dependencies to build.gradle:

def unbluMiddlewareVersion = '2.0.0-SNAPSHOT'

dependencies {
    implementation platform("com.unblu.middleware:unblu-middleware-lib-bom:${unbluMiddlewareVersion}")
    implementation 'com.unblu.middleware:unblu-middleware-lib-spring-webhooks'
    implementation 'com.unblu.openapi:jersey3-client-v4:8.24.0'
}

Configure application.yml:

unblu:
  host: https://some-installation.unblu.com
  api-base-path: /app/rest/v4
  user: some-unblu-admin-user
  password: hello-im-some-unblu-admin-user-password
  middleware:
    url: https://where.my.middleware.is.running
    name: Test middleware
  webhook:
    api-path: /webhook
    secret: hello-im-secure

Register handlers in a Spring bean:

@Service
public class MyWebhookMiddleware implements ApplicationRunner {

    private final WebhookHandler webhookHandler;

    public MyWebhookMiddleware(WebhookHandler webhookHandler) {
        this.webhookHandler = webhookHandler;
    }

    @Override
    public void run(ApplicationArguments args) {
        webhookHandler.on(
                eventName("conversation.new_message"),
                ConversationNewMessageEvent.class,
                event -> System.out.println("Message received: " + event.getConversationMessage().getFallbackText())
        );
        webhookHandler.assertSubscribed();
    }
}

Quick start - dialog bots (Spring Boot)

Add dependencies to build.gradle:

def unbluMiddlewareVersion = '2.0.0-SNAPSHOT'

dependencies {
    implementation platform("com.unblu.middleware:unblu-middleware-lib-bom:${unbluMiddlewareVersion}")
    implementation 'com.unblu.middleware:unblu-middleware-lib-spring-dialog-bot'
    implementation 'com.unblu.openapi:jersey3-client-v4:8.24.0'
}

Configure application.yml:

unblu:
  host: https://some-installation.unblu.com
  api-base-path: /app/rest/v4
  user: some-unblu-admin-user
  password: hello-im-some-unblu-admin-user-password
  middleware:
    url: https://where.my.middleware.is.running
    name: Test middleware
  outbound-requests:
    api-path: /outbound
    secret: hello-im-secure
  bot:
    onboarding-filter: VISITORS
    person:
      first-name: Test
      last-name: Bot
      source-id: Test middleware

Register dialog bot handlers:

@Service
public class MyDialogBotMiddleware implements ApplicationRunner {

    private final DialogBot dialogBot;

    public MyDialogBotMiddleware(DialogBot dialogBot) {
        this.dialogBot = dialogBot;
    }

    @Override
    public void run(ApplicationArguments args) {
        dialogBot.acceptOnboardingOfferIf(_offer -> true);
        dialogBot.onDialogOpen(request -> sendMessage(request.getDialogToken(), "Hello, I am a bot!"));
        dialogBot.onDialogMessage(this::echoIfSentByHuman);
        dialogBot.assertSubscribed();
    }

    private void echoIfSentByHuman(BotDialogMessageRequest request) {
        if (request.getConversationMessage().getSenderPerson().getPersonType() == EPersonType.VISITOR) {
            sendMessage(request.getDialogToken(), "You wrote: " + request.getConversationMessage().getFallbackText());
        }
    }

    private void sendMessage(String dialogToken, String text) {
        System.out.println("Send '" + text + "' to " + dialogToken);
    }
}

Quick start - dialog bots (Quarkus)

Add dependencies to build.gradle:

def unbluMiddlewareVersion = '2.0.0-SNAPSHOT'

dependencies {
    implementation platform("com.unblu.middleware:unblu-middleware-lib-bom:${unbluMiddlewareVersion}")
    implementation 'com.unblu.middleware:unblu-middleware-lib-quarkus-dialog-bot'
    implementation 'com.unblu.openapi:jersey3-client-v4:8.24.0'
}

Register handlers in a CDI bean:

@ApplicationScoped
public class MyDialogBotMiddleware {

    @Inject
    DialogBot dialogBot;

    void onStart(@Observes StartupEvent event) {
        dialogBot.acceptOnboardingOfferIf(_offer -> true);
        dialogBot.onDialogOpen(request -> System.out.println("Dialog opened: " + request.getDialogToken()));
        dialogBot.assertSubscribed();
    }
}

Configuration options

unblu:
  host: https://some-installation.unblu.com # mandatory, set me
  user: superadmin                  # mandatory, set me
  password: hello-im-superadmin-password  # mandatory, set me
  api-base-path: /app/rest/v4   # this is the default
  id-propagation-header-name:
  id-propagation-user-id:       # content of the id propagation header

  middleware:
    url: https://where.my.middleware.is.running
    name: Test middleware
    description: This is a test middleware # optional, but recommended
    self-healing-enabled: true  # see below, this is the default
    self-healing-check-interval-in-seconds: 60 # see below, this is the default
    auto-subscribe: true # if true, the middleware will automatically subscribe to webhooks and bots after startup, otherwise you need to call webhookHandler.subscribe() and dialogBot.subscribe() manually or retrieve the Flux and subscribe to it yourself
    ping-unblu-on-startup: true # if true, the middleware will send a ping to Unblu after startup to verify connectivity; app dies if ping fails

  webhook:
    secret: another-secure-secret  # mandatory if webhooks are used
    clean-previous: false # see below, this is the default
    event-names:    # useful to specify but not needed - event names passed to handlers are registered on the fly
      - conversation.onboarding
      - conversation.new_message

  outbound-requests:
    secret: a-secure-secret  # mandatory if services requiring outbound requests (e.g. dialog bots) are used
    api-path: /outbound   # api path used by the middleware outbound controller, e.g. https://where.my.middleware.is.running/outbound. /outbound this is the default

  bot:
    person:
      source-id: concierge-bot-person-id    # concierge is used as the default bot
      first-name: Unblu
      last-name: Concierge

    clean-previous: false       # see below, this is the default
    onboarding-filter: VISITORS # can be VISITORS, AGENTS, BOTH, or NONE (default)
    onboarding-order: 100       # this is the default
    offboarding-filter: NONE                # can be VISITORS, AGENTS, BOTH, or NONE (default)
    offboarding-order: 100                  # this is the default
    reboarding-enabled: false               # this is the default
    reboarding-order: 100                   # this is the default
    automatic-typing-state-handling-enabled: true  # this is the default
    message-state-handled-externally: false  # this is the default
    needs-counterpart-presence: true        # this is the default
    timeout-in-milli-seconds: 10000         # this is the default
    on-timeout-behavior: ABORT              # can be HAND_OFF or ABORT (default)
    retry-count: 3                          # 0-5
    retry-delay-in-milli-seconds: 1000      # 0-10000

clean-previous: false means that the registration will update the existing webhook registration, if it exists (register for given event names and activate). If you want to remove the previous registration and create a new one, set it to true.

This is useful when after a middleware restart, you don’t want to receive webhook events sent during the middleware downtime. Since Unblu hasn’t received a response to those webhooks, it will try to send them again.

self-healing-enabled: true means that every self-healing-check-interval-in-seconds seconds, the middleware will check and perform repair actions if the webhook and bot registrations are still valid and correctly configured, in particular if they haven’t been auto-disabled by Unblu.

Subscribe

Note that you must subscribe to the fluxes in webhookHandler and outbound handling (outboundHandler / outboundRequestHandler, used by dialogBot).

You can do this by one of the following:

  • Setting unblu.middleware.auto-subscribe=true (default). Library then subscribes on ApplicationReadyEvent, so you must register your handlers before, e.g. in @PostConstruct or @Bean methods or in ApplicationRunner.run() method.

  • Calling .assertSubscribed() methods on the beans, e.g. webhookHandler.assertSubscribed() and dialogBot.assertSubscribed() after registering your handlers. .assertSubscribed() guarantees you’re subscribed exactly once. You can also use explicit .subscribe(), then however you need to take care of double subscriptions.

  • Retrieving the fluxes (.getFlux()) and ensuring they are subscribed after registering your handlers.

Webhook handling

webhookHandler exposes both synchronous and asynchronous handler variants:

  • .on(…​) and .onWrapped(…​) accept Consumer handlers.

  • .onMono(…​) and .onWrappedMono(…​) accept handlers returning Mono<Void>.

Processing order guarantees

Parameter requestOrderSpec determines what order guarantees the library should provide when processing webhooks. It can be one of the following:

  • RequestOrderSpec.canIgnoreOrder() - no order guarantees, the library will process webhooks as they arrive, without any specific order. This is the fastest option, allowing parallel processing of all webhooks.

  • RequestOrderSpec.mustPreserveOrder() - webhook handler functions (and Monos in them) will be called strictly in the order, in which the webhooks were received. You can still launch a parallel processing of an event e.g. by providing a Mono.fromRunnable().publishOn(Schedulers.parallel()) inside the handler function

  • RequestOrderSpec.mustPreserveOrderForThoseWithTheSame(…​) - webhook handler functions will be called in the order the webhooks were received, but only for the webhook calls that have the same value for the specified key. This allows you to process webhooks related to different entities in parallel, while still preserving the order for the same entity (e.g., conversation ID, branch ID, etc.). The entity id/key can be extracted from the event object using the lambda function passed.

Wrapped (with headers)

The processAction (but also other lambdas passed to the same handler call) take the webhook event object as a parameter. In certain cases, headers of the request may be also important for processing (e.g. to propagate in the logback context - see below). For this purpose, the library provides .onWrapped(…​) and .onWrappedMono(…​) method families, which allow you to access the headers of the request in your lambdas, in addition to the event object.

With logback context entries

As an optional last parameter, handling methods also allow you to pass a list of context entry specs, which allows you to populate the logback context with event-related information. Example usage:

webhookHandler.onMono(
        eventName("branch.branch"),
        BranchModificationEvent.class,
        e -> processBranchModified(e),
        canIgnoreOrder(),
        ContextSpec.of(
                "branchId", e -> e.getEntity().getId(),
                "method", _e -> "processBranchModified"
        )
);

logback.xml:

<configuration>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
            <providers>
                <pattern>{"message": "%message %ex","eventId": "%X{eventId}","branchId": "%X{branchId}","method": "%X{method}" ...}</pattern>
            </providers>
        </encoder>
    </appender>
    <springProfile name="production">
        <root level="info">
            <appender-ref ref="STDOUT"/>
        </root>
    </springProfile>
</configuration>

The following logback context variables are available for dialog bot events and outbound requests out-of-the-box (are always populated by the library):

  • eventId

  • deliveryId

  • retryNo

Dialog bots

Dialog bots are a special type of middleware that can interact with Unblu dialogs and conversations. They can be used to automate tasks, provide information, or interact with users in a conversational manner. More details about bots can be found in the Unblu documentation - Bot integration. DialogBotService is still available for compatibility, but deprecated in favor of DialogBot.

Accepting boarding offers

DialogBot allows you to define which onboarding, offboarding, and reboarding offers the bot should accept. You can use the acceptOnboardingOfferIf(), acceptOffboardingOfferIf() and acceptReboardingOfferIf() methods with predicates. For async variants, use the …​Mono methods (for example acceptOnboardingOfferIfMono(…​)). By default, no offers are accepted.

As an Unblu requirement, a bot needs to send a message after a dialog is open (after accepting an offer), before a configured timeout, otherwise it will be disabled by Unblu.

Reacting on dialog events

DialogBot allows you to react to various dialog events, such as dialog opening, dialog messages, and dialog closing. Use onDialog…​() / onWrappedDialog…​() for synchronous handlers (Consumer), or onDialog…​Mono() / onWrappedDialog…​Mono() for reactive handlers returning Mono<Void>. As stated above, you need to implement at least onDialogOpen…​ and send a message in response (see the example app). You can call each function multiple times, e.g. to register handlers in different parts of your middleware application. Processing order of these handlers not guaranteed. Available methods are:

  • onDialogOpen() - called when a dialog is opened (after accepting an onboarding offer)

  • onDialogMessage() - called when a message is sent in a dialog

  • onDialogMessageState() - called when a message state is changed (e.g., when a message is read or delivered)

  • onDialogCounterpartChanged() - called when the counterpart of a dialog changes (e.g., when a user joins or leaves a dialog)

  • onDialogClosed() - called when a dialog is closed

Processing order guarantees

Dialog bot handler guarantees the order of events for the same dialog token. This means that if you have multiple events for the same dialog, they will be processed in the order they were received. However, events for different dialogs can be processed in parallel.

Method flavors

Like the webhook handler, the dialog bot handler also provides a .onWrapped…​() method family, which allows you to access the headers of the request in your lambdas, in addition to the event object. The parameters are the same as for the webhook handler.

Also like the webhook handler, the dialog bot handler allows you to pass a list of context entry specs, which allows you to populate the logback context with event-related information. The usage is the same as for the webhook handler.

The following logback context variables are available for outbound requests out-of-the-box (are always populated by the library):

  • dialogToken

  • conversationId

  • invocationId (for any outbound request)

  • deliveryId (for any outbound request)

  • retryNo (for any outbound request)

Conversation observing bots

Conversation observing bots are implemented using webhooks. You can also use BotPersonRegistrationService to register a bot person to interact with the conversations (write messages).

Outbound request handler API

The library exposes a lower-level api OutboundHandler (with OutboundRequestHandler still available for compatibility), primarily intended for cases which aren’t yet explicitly covered by the library.

In Spring, outbound requests are available transitively with unblu-middleware-lib-spring-dialog-bot, or directly with unblu-middleware-lib-spring-outboundrequests. This exposes outboundHandler (new standard) and outboundRequestHandler (compatibility) beans.

Outbound requests are handled similarly to webhooks, however require a proper response in the form of a class <Xxx>Response for each <Xxx>Request. Like with webhooks, the general practice is to respond as quickly as possible, and perform longer processing asynchronously. For outbound requests however, this may not always be possible, because an actual response with results of the handler operation is sometimes needed. For that reason, outboundHandler.on(…​) supports synchronous response/async handlers, and outboundHandler.onWrappedMono(…​) supports reactive response/async handlers.

Example usage - this is how the dialog bot service implements onDialogOpen():

outboundHandler.onWrappedMono(
        outboundRequestType("outbound.bot.dialog.opened"),
        BotDialogOpenRequest.class,
        BotDialogOpenResponse.class,
        _request -> Mono.just(new BotDialogOpenResponse())
                .doOnNext(_response -> log.debug("Responding to bot dialog open")),
        request -> Mono.fromRunnable(() -> sendMessage(request.getDialogToken(), "Hello, I am a bot!")),
        OutboundRequestHandlerOptions.requestOrderSpec(
                        mustPreserveOrderForThoseWithTheSame(request -> request.getDialogToken()))
                .withContextSpec(ContextSpec.of(
                        "dialogToken", request -> request.getDialogToken()
                )));

sendMessage() here is an expensive asynchronous operation, so it is performed in the asynchronous handler lambda, while the synchronous lambda just returns an empty BotDialogOpenResponse as quickly as possible.

Method flavors

Like the webhook handler, the outbound request handler also provides a .onWrapped…​() method family, which allows you to access the headers of the request in your lambdas, in addition to the event object.

Also like the webhook handler, the dialog bot handler allows you to pass a list of context entry specs, which allows you to populate the logback context with event-related information. The usage is the same as for the webhook handler. Deprecated overloads with explicit requestOrderSpec and contextSpec parameters are still available for backward compatibility.

The following logback context variables are available for webhooks out-of-the-box (are always populated by the library):

  • invocationId

  • deliveryId

  • retryNo

Consume the middleware-lib from jitpack

The service https://jitpack.io/ is able to build any commit from any open source repo.

Warning

Jars on the jitpack repositories are not immutable (like on a SNAPSHOT repository) and the build of the jar is delegated to an external service. It is not recommended to use them in a middleware application that goes to production.

An additional repository has to be declared in the repositories section:

maven {
    url "https://jitpack.io"
    content {
        includeGroup "com.github.unblu"
    }
}

The coordinates are different:

  • GroupId: com.github.unblu

  • ArtifactId: one module artifact, for example unblu-middleware-lib-bom, unblu-middleware-lib-spring-webhooks, or unblu-middleware-lib-quarkus-dialog-bot

  • Version: a commit (for example e8f15d5ef4), a tag (for example 1.8.1) or a branch name (for example main-SNAPSHOT)

Troubleshoot a build on jitpack:

Troubleshooting

Configuration issues

Property must not be blank - you must populate required properties in your application.yml file, such as unblu.host, unblu.user, unblu.password, unblu.middleware.url, unblu.middleware.name, and either unblu.webhook.secret or unblu.outbound-requests.secret.

Errors during registration management

Errors during webhook registration management (typically 403 forbidden) are usually caused either by wrong Unblu credentials, or by using a non-admin Unblu user.

Handlers are not triggered

This is usually caused by not subscribing, see the "Subscribe" section above.