Можно ли интегрировать Bot-Builder в существующее экспресс-приложение? - PullRequest
0 голосов
/ 29 апреля 2018

У меня есть существующее приложение узла / экспресс-чата, которое подключается к нескольким чат-платформам, используя шаблон проектирования промежуточного программного обеспечения ExpressJS next (), next (). Я отправляю ответ 200 в самом начале, чтобы подтвердить получение сообщения, и отправляю новый запрос POST, чтобы отправить сообщение из моего последнего промежуточного программного обеспечения.

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

Теперь я хочу интегрировать Skype как канал для своего бота, но библиотека NodeJS для bot-framework имеет совершенно другой способ работы, используя события и такую ​​магию, которую я еще не полностью понял:

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);
});

Не похоже, что это совместимые способы выполнения действий, так как лучше всего получить сообщение, а затем отправить ответ пользователю, не меняя мою экспресс-маршрутизацию, чтобы она соответствовала бот-сборщику? Могу ли я получить объект Session с помощью Session.send () для ответа? Придется ли делать всю адресацию вручную? Есть ли способ, который похож на это:

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 голосов
/ 03 мая 2018

Создание сообщений, их адресация и отправка этих сообщений возможны с помощью официальной библиотеки фреймворков NodeJS. Что я не мог сделать с этой библиотекой, так это получать сообщения и проверять их подлинность на моих маршрутах, не внося существенных изменений в мой дизайн (используя промежуточное программное обеспечение для запросов - next () - для обработки входящего запроса), которое уже работает с другими ботами и не легко изменить.

Вот мой обходной путь: во-первых, это класс BotFrameworkAuthenticator, который я создаю на основе документации Microsoft здесь: https://docs.microsoft.com/en-us/azure/bot-service/rest-api/bot-framework-rest-connector-authentication Он создается с помощью appID и appPassword из приложения Bot Framework.

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 Framework без наличия объекта Session, который обычно создается библиотекой Bot Framework при получении входящего сообщения:

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 () и т. Д.), А не только текст ()

0 голосов
/ 30 апреля 2018

Вы можете зарегистрировать приложение бота в существующих экспресс-приложениях. Bot Builder SDK также совместим с платформой expressjs. Вы можете сослаться на официальный образец , который также использует экспресс.

Не забудьте изменить конечную точку обмена сообщениями в регистрации вашего бота для конечной точки вашего бота, например,

https://yourdomain/stuff

в вашем сценарии. Пожалуйста, обратитесь к https://docs.microsoft.com/en-us/azure/bot-service/bot-service-quickstart-registration для получения дополнительной информации.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...