voxa
Version:
A fsm (state machine) framework for Alexa, Dialogflow, Facebook Messenger and Botframework apps using Node.js
258 lines (223 loc) • 7.59 kB
text/typescript
/*
* Copyright (c) 2018 Rain Agency <contact@rain.agency>
* Author: Rain Agency <contact@rain.agency>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import {
IAttachment,
IBotStorageContext,
IBotStorageData,
IChatConnectorAddress,
IEvent,
IIdentity,
ISuggestedActions,
} from "botbuilder";
import * as _ from "lodash";
import * as rp from "request-promise";
import urljoin = require("url-join");
import * as uuid from "uuid";
import { NotImplementedError } from "../../errors";
import { IBag, IVoxaEvent } from "../../VoxaEvent";
import { addToSSML, addToText, IVoxaReply } from "../../VoxaReply";
import { BotFrameworkEvent } from "./BotFrameworkEvent";
import { IAuthorizationResponse } from "./BotFrameworkInterfaces";
import { BotFrameworkPlatform } from "./BotFrameworkPlatform";
export class BotFrameworkReply implements IVoxaReply {
public get hasMessages(): boolean {
return !!this.speak || !!this.text;
}
public get hasDirectives(): boolean {
return !!this.attachments || !!this.suggestedActions;
}
public get hasTerminated(): boolean {
return this.inputHint === "acceptingInput";
}
public get speech(): string {
if (!this.speak) {
return "";
}
return this.speak;
}
// IMessage
public channelId: string;
public conversation: IIdentity;
public from: IIdentity;
public id?: string;
public inputHint: string;
public locale: string;
public recipient: IIdentity;
public replyToId?: string;
public speak: string = "";
public text: string = "";
public textFormat: string = "plain";
public timestamp: string;
public type: string = "message";
public attachments?: IAttachment[];
public suggestedActions?: ISuggestedActions;
public attachmentLayout?: string;
constructor(private event: BotFrameworkEvent) {
this.channelId = event.rawEvent.message.address.channelId;
if (!event.session) {
throw new Error("event.session is missing");
}
this.conversation = { id: event.session.sessionId };
this.from = { id: event.rawEvent.message.address.bot.id };
this.inputHint = "ignoringInput";
this.locale = event.request.locale;
if (!event.user) {
throw new Error("event.user is missing");
}
this.recipient = {
id: event.user.id,
};
if (event.user.name) {
this.recipient.name = event.user.name;
}
this.replyToId = (event.rawEvent.message
.address as IChatConnectorAddress).id;
this.timestamp = new Date().toISOString();
}
public toJSON() {
return _.omit(this, "event");
}
public clear() {
this.attachments = undefined;
this.suggestedActions = undefined;
this.text = "";
this.speak = "";
}
public terminate() {
this.inputHint = "acceptingInput";
}
public addStatement(statement: string, isPlain: boolean = false) {
if (this.inputHint === "ignoringInput") {
this.inputHint = "expectingInput";
}
if (isPlain) {
this.text = addToText(this.text, statement);
} else {
this.speak = addToSSML(this.speak, statement);
}
}
public hasDirective(type: string | RegExp): boolean {
throw new NotImplementedError("hasDirective");
}
public addReprompt(reprompt: string) {
return;
}
public async send() {
this.event.log.debug("partialReply", {
hasDirectives: this.hasDirectives,
hasMessages: this.hasMessages,
sendingPartialReply: !(!this.hasMessages && !this.hasDirectives),
});
if (!this.hasMessages && !this.hasDirectives) {
return;
}
const uri = this.getReplyUri(this.event.rawEvent.message);
this.id = uuid.v1();
await this.botApiRequest("POST", uri, _.clone(this), this.event);
this.clear();
}
public async botApiRequest(
method: string,
uri: string,
reply: BotFrameworkReply,
event: BotFrameworkEvent,
attempts: number = 0,
): Promise<any> {
let authorization: IAuthorizationResponse;
const platform = event.platform as BotFrameworkPlatform;
authorization = await this.getAuthorization(
platform.applicationId,
platform.applicationPassword,
);
const requestOptions: rp.Options = {
auth: {
bearer: authorization.access_token,
},
body: this,
json: true,
method,
uri,
};
event.log.debug("botApiRequest", { requestOptions });
return rp(requestOptions);
}
public getReplyUri(event: IEvent): string {
const address: IChatConnectorAddress = event.address;
const baseUri = address.serviceUrl;
if (!baseUri || !address.conversation) {
throw new Error("serviceUrl is missing");
}
const conversationId = encodeURIComponent(address.conversation.id);
let path = `/v3/conversations/${conversationId}/activities`;
if (address.id) {
path += "/" + encodeURIComponent(address.id);
}
return urljoin(baseUri, path);
}
public async getAuthorization(
applicationId?: string,
applicationPassword?: string,
): Promise<IAuthorizationResponse> {
const url =
"https://login.microsoftonline.com/botframework.com/oauth2/v2.0/token";
const requestOptions: rp.Options = {
form: {
client_id: applicationId,
client_secret: applicationPassword,
grant_type: "client_credentials",
scope: "https://api.botframework.com/.default",
},
json: true,
method: "POST",
url,
};
return (await rp(requestOptions)) as IAuthorizationResponse;
}
public async saveSession(attributes: IBag, event: IVoxaEvent): Promise<void> {
const storage = (event.platform as BotFrameworkPlatform).storage;
const conversationId = encodeURIComponent(event.session.sessionId);
const userId = event.rawEvent.message.address.bot.id;
const context: IBotStorageContext = {
conversationId,
persistConversationData: false,
persistUserData: false,
userId,
};
const data: IBotStorageData = {
conversationData: {},
// we're only gonna handle private conversation data, this keeps the code small
// and more importantly it makes it so the programming model is the same between
// the different platforms
privateConversationData: attributes,
userData: {},
};
await new Promise((resolve, reject) => {
storage.saveData(context, data, (error: Error) => {
if (error) {
return reject(error);
}
event.log.debug("savedStateData", { data, context });
return resolve();
});
});
}
}