Skip to content
This repository was archived by the owner on Dec 4, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MT License.
package com.microsoft.bot.builder;

import java.io.IOException;
import java.util.concurrent.CompletableFuture;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import com.microsoft.bot.connector.Channels;
import com.microsoft.bot.schema.Activity;
import com.microsoft.bot.schema.ActivityTypes;

import org.apache.commons.lang3.StringUtils;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;

/**
* Support the DirectLine speech and telephony channels to ensure the
* appropriate SSML tags are set on the Activity Speak property.
*/
public class SetSpeakMiddleware implements Middleware {

private final String voiceName;
private final boolean fallbackToTextForSpeak;
private final String lang;

/**
* Initializes a new instance of the {@link SetSpeakMiddleware} class.
*
* @param voiceName The SSML voice name attribute value.
* @param lang The xml:lang value.
* @param fallbackToTextForSpeak true if an empt Activity.Speak is populated
* with Activity.getText().
*/
public SetSpeakMiddleware(String voiceName, String lang, boolean fallbackToTextForSpeak) {
this.voiceName = voiceName;
this.fallbackToTextForSpeak = fallbackToTextForSpeak;
if (lang == null) {
throw new IllegalArgumentException("lang cannot be null.");
} else {
this.lang = lang;
}
}

/**
* Processes an incoming activity.
*
* @param turnContext The context Object for this turn.
* @param next The delegate to call to continue the bot middleware
* pipeline.
*
* @return A task that represents the work queued to execute.
*/
public CompletableFuture<Void> onTurn(TurnContext turnContext, NextDelegate next) {
turnContext.onSendActivities((ctx, activities, nextSend) -> {
for (Activity activity : activities) {
if (activity.getType().equals(ActivityTypes.MESSAGE)) {
if (fallbackToTextForSpeak && StringUtils.isBlank(activity.getSpeak())) {
activity.setSpeak(activity.getText());
}

if (StringUtils.isNotBlank(activity.getSpeak()) && StringUtils.isNotBlank(voiceName)
&& (StringUtils.compareIgnoreCase(turnContext.getActivity().getChannelId(),
Channels.DIRECTLINESPEECH) == 0
|| StringUtils.compareIgnoreCase(turnContext.getActivity().getChannelId(),
Channels.EMULATOR) == 0
|| StringUtils.compareIgnoreCase(turnContext.getActivity().getChannelId(),
"telephony") == 0)) {
if (!hasTag("speak", activity.getSpeak()) && !hasTag("voice", activity.getSpeak())) {
activity.setSpeak(
String.format("<voice name='%s'>%s</voice>", voiceName, activity.getSpeak()));
}

activity.setSpeak(String
.format("<speak version='1.0' xmlns='http://www.w3.org/2001/10/synthesis' "
+ "xml:lang='%s'>%s</speak>", lang, activity.getSpeak()));
}
}
}
return nextSend.get();
});
return next.next();
}

private boolean hasTag(String tagName, String speakText) {
try {
DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
Document doc = dBuilder.parse(speakText);

if (doc.getElementsByTagName(tagName).getLength() > 0) {
return true;
}

return false;
} catch (SAXException | ParserConfigurationException | IOException ex) {
return false;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MT License.

package com.microsoft.bot.builder;

import com.microsoft.bot.builder.adapters.TestAdapter;
import com.microsoft.bot.builder.adapters.TestFlow;
import com.microsoft.bot.connector.Channels;
import com.microsoft.bot.schema.Activity;
import com.microsoft.bot.schema.ChannelAccount;
import com.microsoft.bot.schema.ConversationAccount;
import com.microsoft.bot.schema.ConversationReference;

import org.junit.Assert;
import org.junit.Test;

public class SetSpeakMiddlewareTests {

@Test
public void ConstructorValidation() {
// no 'lang'
Assert.assertThrows(IllegalArgumentException.class, () -> new SetSpeakMiddleware("voice", null, false));
}

@Test
public void NoFallback() {
TestAdapter adapter = new TestAdapter(createConversation("NoFallback"))
.use(new SetSpeakMiddleware("male", "en-us", false));

new TestFlow(adapter, turnContext -> {
Activity activity = MessageFactory.text("OK");

return turnContext.sendActivity(activity).thenApply(result -> null);
}).send("foo").assertReply(obj -> {
Activity activity = (Activity) obj;
Assert.assertNull(activity.getSpeak());
}).startTest().join();
}

// fallback instanceof true, for any ChannelId other than emulator,
// directlinespeech, or telephony should
// just set Activity.Speak to Activity.Text if Speak instanceof empty.
@Test
public void FallbackNullSpeak() {
TestAdapter adapter = new TestAdapter(createConversation("NoFallback"))
.use(new SetSpeakMiddleware("male", "en-us", true));

new TestFlow(adapter, turnContext -> {
Activity activity = MessageFactory.text("OK");

return turnContext.sendActivity(activity).thenApply(result -> null);
}).send("foo").assertReply(obj -> {
Activity activity = (Activity) obj;
Assert.assertEquals(activity.getText(), activity.getSpeak());
}).startTest().join();
}

// fallback instanceof true, for any ChannelId other than emulator,
// directlinespeech, or telephony should
// leave a non-empty Speak unchanged.
@Test
public void FallbackWithSpeak() {
TestAdapter adapter = new TestAdapter(createConversation("Fallback"))
.use(new SetSpeakMiddleware("male", "en-us", true));

new TestFlow(adapter, turnContext -> {
Activity activity = MessageFactory.text("OK");
activity.setSpeak("speak value");

return turnContext.sendActivity(activity).thenApply(result -> null);
}).send("foo").assertReply(obj -> {
Activity activity = (Activity) obj;
Assert.assertEquals("speak value", activity.getSpeak());
}).startTest().join();
}

@Test
public void AddVoiceEmulator() {
AddVoice(Channels.EMULATOR);
}

@Test
public void AddVoiceDirectlineSpeech() {
AddVoice(Channels.DIRECTLINESPEECH);
}

@Test
public void AddVoiceTelephony() {
AddVoice("telephony");
}


// Voice instanceof added to Speak property.
public void AddVoice(String channelId) {
TestAdapter adapter = new TestAdapter(createConversation("Fallback", channelId))
.use(new SetSpeakMiddleware("male", "en-us", true));

new TestFlow(adapter, turnContext -> {
Activity activity = MessageFactory.text("OK");

return turnContext.sendActivity(activity).thenApply(result -> null);
}).send("foo").assertReply(obj -> {
Activity activity = (Activity) obj;
Assert.assertEquals("<speak version='1.0' xmlns='http://www.w3.org/2001/10/synthesis' "
+ "xml:lang='en-us'><voice name='male'>OK</voice></speak>",
activity.getSpeak());
}).startTest().join();
}

@Test
public void AddNoVoiceEmulator() {
AddNoVoice(Channels.EMULATOR);
}

@Test
public void AddNoVoiceDirectlineSpeech() {
AddNoVoice(Channels.DIRECTLINESPEECH);
}

@Test
public void AddNoVoiceTelephony() {
AddNoVoice("telephony");
}


// With no 'voice' specified, the Speak property instanceof unchanged.
public void AddNoVoice(String channelId) {
TestAdapter adapter = new TestAdapter(createConversation("Fallback", channelId))
.use(new SetSpeakMiddleware(null, "en-us", true));

new TestFlow(adapter, turnContext -> {
Activity activity = MessageFactory.text("OK");

return turnContext.sendActivity(activity).thenApply(result -> null);
}).send("foo").assertReply(obj -> {
Activity activity = (Activity) obj;
Assert.assertEquals("OK",
activity.getSpeak());
}).startTest().join();
}


private static ConversationReference createConversation(String name) {
return createConversation(name, "User1", "Bot", "test");
}

private static ConversationReference createConversation(String name, String channelId) {
return createConversation(name, "User1", "Bot", channelId);
}

private static ConversationReference createConversation(String name, String user, String bot, String channelId) {
ConversationReference conversationReference = new ConversationReference();
conversationReference.setChannelId(channelId);
conversationReference.setServiceUrl("https://test.com");
conversationReference.setConversation(new ConversationAccount(false, name, name));
conversationReference.setUser(new ChannelAccount(user.toLowerCase(), user));
conversationReference.setBot(new ChannelAccount(bot.toLowerCase(), bot));
conversationReference.setLocale("en-us");
return conversationReference;
}
}