是否可以将bot-builder集成到现有的Express应用程序中?

时间:2018-04-29 07:18:17

标签: node.js events botframework

我有一个现有的节点/快速聊天机器人应用程序,可以使用ExpressJS连接到多个聊天平台。 next(),next()中间件设计模式。我在最开始发送200响应以确认收到消息,并发送新的POST请求以从我的上一个中间件发送消息。

app.post("/bots", receiveMsg, doStuff, formatAndSendMsg, catchErrors);

现在我想将Skype整合为我的机器人的渠道,但是bot-framework的NodeJS库有一种完全不同的做事方式,使用事件和我尚未完全理解的魔术: / p>

var connector = new builder.ConsoleConnector();
app.post("/skype", connector.listen());
var bot = new builder.UniversalBot(connector, function (session) {
    session.send("You said: %s", session.message.text);
});

看起来这些都不是兼容的方式,所以接收消息然后向用户发送响应的最佳方式是什么,而无需更改我的快速路由以适应bot-builder ?我可以使用Session.send()获取Session对象来响应吗?我是否必须手动完成所有寻址?有没有类似的方法:

app.post("/skype", (req, res, next) => {
    const address = req.body.id;
    const message = new builder.Message(address, messageBody).send()
}

或者:

app.post("/skype", connector.listen(), (req, res, next) => {
    // (res.locals is available in every express middleware function)
    const session = res.locals.botFrameworkSession;
    // do stuff
    session.send(message);
}

2 个答案:

答案 0 :(得分:1)

您可以在现有快递应用程序中注册bot应用程序。 Bot builder SDK在expressjs框架中也兼容。您可以参考同时利用快递的official sample

不要忘记将机器人注册中的消息端点修改为机器人的终端,例如:

https://yourdomain/stuff
在您的方案中

。有关详细信息,请参阅https://docs.microsoft.com/en-us/azure/bot-service/bot-service-quickstart-registration

答案 1 :(得分:0)

使用官方bot框架NodeJS库可以构建消息,解决它们并发送这些消息。我无法对该库进行的操作是接收消息并在我的路由上验证其真实性,而不对我的设计进行重大更改(使用请求中间件 - next() - 处理传入的请求)已经在生产中与其他机器人并不容易改变。

以下是我的解决方法:首先是我根据此处的Microsoft文档创建的BotFrameworkAuthenticator类:https://docs.microsoft.com/en-us/azure/bot-service/rest-api/bot-framework-rest-connector-authentication 它使用您的Bot Framework应用程序中的appID和appPassword进行实例化。



import axios from "axios";
import * as jwt from "jsonwebtoken";
import * as jwkToPem from 'jwk-to-pem';

export class BotFrameworkAuthenticator {
    private appId: string;
    private appPassword: string;
    private openIdMetadata: any;
    // The response body specifies the document in the JWK format but also includes an additional property for each key: endorsements.
    private validSigningKeys: any;
    // The list of keys is relatively stable and may be cached for long periods of time (by default, 5 days within the Bot Builder SDK).
    private signingKeyRefreshRate: number = 432000; // in seconds (432000 = 5 days)

    constructor(appId, appPassword) {
        this.appId = appId;
        this.appPassword = appPassword;
        this.getListOfSigningKeys();
    }

    // response data should contain "jwks_uri" property that contains address to request list of valid signing keys.
    public async getOpenIdMetaData() {
        // This is a static URL that you can hardcode into your application. - MS Bot Framework docs
        await axios.get("https://login.botframework.com/v1/.well-known/openidconfiguration").then(response => {
            this.openIdMetadata = response.data;
            logger.info("OpenID metadata document recieved for Bot Framework.");
        }).catch(err => {
            logger.warn(err.message, "Could not get OpenID metadata document for Bot Framework. Retrying in 15 seconds...");
            setTimeout(this.getListOfSigningKeys, 15000);
        })
    }

    public async getListOfSigningKeys() {
        await this.getOpenIdMetaData();
        if (this.openIdMetadata && this.openIdMetadata.jwks_uri) {
            // previous function getOpenIdMetaData() succeeded
            await axios.get(this.openIdMetadata.jwks_uri).then(response => {
                logger.info(`Signing keys recieved for Bot Framework. Caching for ${this.signingKeyRefreshRate / 86400} days.`);
                this.validSigningKeys = response.data.keys;
                setTimeout(this.getListOfSigningKeys, (this.signingKeyRefreshRate * 1000));
            }).catch(err => {
                logger.error(err.message, "Could not get list of valid signing keys for Bot Framework. Retrying in 15 seconds");
                setTimeout(this.getListOfSigningKeys, 15000);
            });
        } else {
            // previous function getOpenIdMetaData() failed, but has already queued this function to run again. Will continue until succeeds.
            return;
        }
    }

    /**
     * Verifies that the message was sent from Bot Framework by checking values as specified in Bot Framework Documentation:
     * https://docs.microsoft.com/en-us/azure/bot-service/rest-api/bot-framework-rest-connector-authentication#step-4-verify-the-jwt-token
     * Retrieves the Bearer token from the authorization header, decodes the token so we can match the key id (kid) to a key in the OpenID
     * document, then converts that key to PEM format so that jwt/crypto can consume it to verify that the bearer token is
     * cryptographically signed.
     * If the serviceUrl property in the token doe not match the serviceUrl property in the message, it should also be rejected.
     */
    public verifyMsgAuthenticity(serviceUrl: string, headers: any) {
        try {
            const token = headers.authorization.replace("Bearer ", "");
            const decoded = jwt.decode(token, { complete: true }) as any;
            const verifyOptions = {
                issuer: "https://api.botframework.com",
                audience: this.appId,
                clockTolerance: 300, // (seconds) The token is within its validity period. Industry-standard clock-skew is 5 minutes. (Bot Framework documentation);
            }
            const jwk = this.lookupKey(decoded.header.kid)
            const pem = jwkToPem(jwk);
            const verified = jwt.verify(token, pem, verifyOptions) as any;
            if (!serviceUrl || serviceUrl !== verified.serviceurl) {
                logger.warn("Non-matching serviceUrl in Bot Framework verified token!")
                return false;
            }
            return true;
        } catch (err) {
            logger.warn("Received invalid/unsigned message on Bot Framework endpoint!", err.message)
            return false;
        }
    }

    // Finds the relevant key from the openID list. Does not transform the key.
    private lookupKey(kid) {
        const jwk = this.validSigningKeys.find((key) => {
            return (key.kid === kid);
        });
        return jwk;
    }
}




在快速路线的最开始使用这样的BotFrameworkAuthenticator类来验证所有传入的请求是否有效。



const botFrameworkAuthenticator = new BotFrameworkAuthenticator(appID, appPassword);

router.post("/", (req: Request, res: Response, next: NextFunction) => {
    if (botFrameworkAuthenticator.verifyMsgAuthenticity(req.body.serviceUrl, req.headers) === true) {
        res.status(200).send();
        next();
    } else {
        // unsafe to process
        res.status(403).send();
        return;
    }
});




使用常规Bot框架库发送消息,而不会在收到传入消息时通常由Bot Framework库创建的Session对象:



import * as builder from "botbuilder";

// instantiate the chatConnector (only once, not in the same function as the sending occurs)
const botFrameworkSender = new builder.ChatConnector({ appId, appPassword });

//---------------------------------------------

const skypeMsg = req.body;
const address = {
    channelId: skypeMsg.channelId,
    user: skypeMsg.from,
    bot: skypeMsg.recipient,
    conversation: skypeMsg.conversation
};
const response = new builder.Message().text(someText).address(address).toMessage();
const formattedResponses = [response];
botFrameworkSender.send(formattedResponses, logErrorsToConsole);




请注意,可以使用所有builder.Message() - .attachment(),. images()等... - 函数,而不仅仅是text()