UNPKG

@broid/kit

Version:

Bot framework supported all messaging plateforms and middlewares.

343 lines (342 loc) 14.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const utils_1 = require("@broid/utils"); const Promise = require("bluebird"); const bodyParser = require("body-parser"); const express = require("express"); const R = require("ramda"); const Rx_1 = require("rxjs/Rx"); const isObservable = (obs) => obs && typeof obs.subscribe === 'function'; const isPromise = (obj) => obj && (typeof obj === 'object') && ('tap' in obj) && ('then' in obj) && (typeof obj.then === 'function'); class Bot { constructor(obj) { this.logLevel = obj && obj.logLevel || 'info'; this.integrations = []; this.incomingMiddlewares = []; this.outgoingMiddlewares = []; this.router = express.Router(); this.httpEndpoints = []; this.httpServer = null; if (obj && obj.http) { this.startHttpServer(obj.http); } this.logger = new utils_1.Logger('broidkit', this.logLevel); } getHTTPEndpoints() { return this.httpEndpoints; } getRouter() { if (this.httpServer) { return null; } return this.router; } use(instance, filter) { if (instance.listen) { this.logger.info({ method: 'use', message: `Integration: ${instance.serviceName()}` }); this.addIntegration(instance); } else if (instance.incoming) { this.logger .info({ method: 'use', message: `incoming middleware: ${instance.serviceName()}` }); this.incomingMiddlewares.push({ filter: filter || null, middleware: instance, name: `${instance.serviceName()}.incoming`, }); } else if (instance.outgoing) { this.logger .info({ method: 'use', message: `outgoing middleware: ${instance.serviceName()}` }); this.outgoingMiddlewares.push({ filter: filter || null, middleware: instance, name: `${instance.serviceName()}.outgoing`, }); } return; } hear(pattern, messageTypes, cb) { const args = this.processArgs(messageTypes, cb); const messageTypesArr = this.messageTypes2Arr(R.prop('msgTypes', args)); let patternRegex = false; if (typeof (pattern) === 'string') { patternRegex = new RegExp(pattern, 'ig'); } else { patternRegex = pattern; } const listener = Rx_1.Observable .merge(...R.flatten(R.map((integration) => [integration.connect(), integration.listen()], this.integrations))) .mergeMap((message) => this.processIncomingMessage(message)) .mergeMap((messageUpdated) => this.testIncoming(messageUpdated.message, patternRegex, messageTypesArr) ? Promise.resolve(messageUpdated) : Rx_1.Observable.empty()); return this.processListener(listener, R.prop('callback', args)); } hears(patterns, messageTypes, cb) { const args = this.processArgs(messageTypes, cb); const messageTypesArr = this.messageTypes2Arr(R.prop('msgTypes', args)); const patternRegexes = R.map((pattern) => new RegExp(pattern, 'ig'), patterns); const listener = Rx_1.Observable .merge(...R.flatten(R.map((integration) => [integration.connect(), integration.listen()], this.integrations))) .mergeMap((message) => this.processIncomingMessage(message)) .mergeMap((messageUpdated) => { const matches = R.pipe(R.map((patternRegex) => this.testIncoming(messageUpdated.message, patternRegex, messageTypesArr)), R.reject(R.equals(false))); if (!R.isEmpty(matches(patternRegexes))) { return Promise.resolve(messageUpdated); } return Rx_1.Observable.empty(); }); return this.processListener(listener, R.prop('callback', args)); } on(messageTypes, cb) { return this.hear(true, messageTypes, cb); } sendText(text, message) { return this.processOutgoingContent(text, message) .then((updated) => { const content = updated.content || text; let data = { '@context': 'https://www.w3.org/ns/activitystreams', 'generator': { id: R.path(['generator', 'id'], message), name: R.path(['generator', 'name'], message), type: 'Service', }, 'object': { content, id: R.path(['object', 'id'], message), type: 'Note', }, 'to': { id: R.path(['target', 'id'], message), type: R.path(['target', 'type'], message), }, 'type': 'Create', }; data = this.addMessageContext(data, message); return this.send(data); }); } sendVideo(url, message, meta) { return this.sendMedia(url, 'Video', message, meta); } sendImage(url, message, meta) { return this.sendMedia(url, 'Image', message, meta); } processOutgoingContent(content, message) { return this.processOutgoingMessage(content, message) .toPromise(Promise) .then((updated) => { const contents = R.reject(R.isNil)(R.map((o) => o.content, updated.data)); if (!R.isEmpty(contents)) { updated.content = R.join(' ', contents); } return updated; }); } messageTypes2Arr(messageTypes) { let messageTypesArr = []; if (messageTypes) { messageTypesArr = R.map((m) => R.toLower(m.replace(/^\s+|\s+$/g, '')), R.split(',', messageTypes)); } return messageTypesArr; } processArgs(msgTypes, cb) { if (R.is(Function, msgTypes)) { return { callback: msgTypes, }; } return { callback: cb, msgTypes: msgTypes, }; } processListener(listener, callback) { if (callback) { listener.subscribe(callback, (error) => callback(null, error)); } return listener; } testIncoming(message, patternRegex, messageTypesArr) { const messageContext = R.prop('@context', message); if (!messageContext) { this.logger.debug('Message incoming should follow Broid schema.', message); return false; } const content = R.path(['object', 'content'], message); const targetType = R.toLower(R.path(['target', 'type'], message)); if (R.isEmpty(messageTypesArr) || R.contains(targetType, messageTypesArr)) { if (patternRegex instanceof RegExp) { const isMatch = patternRegex.test(content); patternRegex.lastIndex = 0; if (isMatch === true) { return true; } } else if (patternRegex === true) { return true; } } return false; } send(data) { const to = R.path(['to', 'id'], data); const toType = R.path(['to', 'type'], data); const serviceID = R.path(['generator', 'id'], data); const serviceName = R.path(['generator', 'name'], data); if (to && toType && serviceID && serviceName) { const integrationFind = R.filter((integration) => integration.serviceId() === serviceID, this.integrations); if (!R.isEmpty(integrationFind)) { return integrationFind[0].send(data); } return Promise.reject(`Integration ${serviceID} not found.`); } return Promise.reject('Message should follow broid-schemas.'); } sendMedia(url, mediaType, message, meta = {}) { return this.processOutgoingContent(url, message) .then((updated) => { const urlUpdated = updated.content || url; let data = { '@context': 'https://www.w3.org/ns/activitystreams', 'generator': { id: R.path(['generator', 'id'], message), name: R.path(['generator', 'name'], message), type: 'Service', }, 'object': { content: R.prop('content', meta) || '', id: R.path(['object', 'id'], message), name: R.prop('name', meta) || '', title: R.prop('title', meta) || '', type: mediaType, url: urlUpdated, }, 'to': { id: R.path(['target', 'id'], message), type: R.path(['target', 'type'], message), }, 'type': 'Create', }; data = this.addMessageContext(data, message); return this.send(data); }); } addIntegration(integration) { this.integrations.push(integration); if (!integration.getRouter) { return; } const router = integration.getRouter(); if (router) { const httpPath = `/webhook/${integration.serviceName()}`; this.httpEndpoints.push(httpPath); this.router.use(httpPath, router); } return; } chain(input, filters) { const seq = Rx_1.Observable.from(filters); return seq.reduce((chain, filter, index) => { return chain.concatMap((data) => { return filter(data) .map((filterResult) => { return R.flatten(R.concat(data, [R.assoc('order', index, filterResult)])); }); }); }, Rx_1.Observable.of(input)) .concatMap((value) => value); } processIncomingMessage(message) { const middlewares = R.map((middleware) => { return (acc) => { let resultObservable = Rx_1.Observable.empty(); let patternRegexes = []; if (middleware.filter) { const patterns = R.is(Array, middleware.filter) ? middleware.filter : [middleware.filter]; patternRegexes = R.map((pattern) => new RegExp(pattern, 'ig'), patterns); } const matches = R.pipe(R.map((patternRegex) => this.testIncoming(message, patternRegex, [])), R.reject(R.equals(false))); if (R.isEmpty(patternRegexes) || !R.isEmpty(matches(patternRegexes))) { const fn = middleware.middleware.incoming; const result = fn(this, message, acc); if (isObservable(result)) { resultObservable = result; } else if (isPromise(result)) { resultObservable = Rx_1.Observable.fromPromise(result); } else { resultObservable = Rx_1.Observable.of(result); } } return resultObservable.map((data) => ({ middleware: middleware.name, data })); }; }, this.incomingMiddlewares); const intialAcc = []; return this.chain(intialAcc, middlewares) .take(1) .map((data) => ({ data, message })); } processOutgoingMessage(content, message) { const middlewares = R.map((middleware) => { return (acc) => { let resultObservable = Rx_1.Observable.empty(); let patternRegexes = []; if (middleware.filter) { const patterns = R.is(Array, middleware.filter) ? middleware.filter : [middleware.filter]; patternRegexes = R.map((pattern) => new RegExp(pattern, 'ig'), patterns); } const matches = R.pipe(R.map((patternRegex) => this.testIncoming(message, patternRegex, [])), R.reject(R.equals(false))); if (R.isEmpty(patternRegexes) || !R.isEmpty(matches(patternRegexes))) { const fn = middleware.middleware.outgoing; const result = fn(this, content, message, acc); if (isObservable(result)) { resultObservable = result; } else if (isPromise(result)) { resultObservable = Rx_1.Observable.fromPromise(result); } else { resultObservable = Rx_1.Observable.of(result); } } return resultObservable.map((d) => { let data = d; if (typeof data === 'string') { data = { content: data, }; } return { middleware: middleware.name, data, content: data.content }; }); }; }, this.outgoingMiddlewares); const intialAcc = []; return this.chain(intialAcc, middlewares) .take(1) .map((data) => ({ data, message })); } startHttpServer(httpOptions) { if (!this.httpServer) { const app = express(); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: false })); app.use(this.router); this.httpServer = app.listen(httpOptions.port, httpOptions.host, () => { this.logger.info(`Server listening on port ${httpOptions.host}:${httpOptions.port}...`); }); } } addMessageContext(data, message) { const context = R.path(['object', 'context'], message); if (context) { data.object = R.assoc('context', context, data.object); } return data; } } exports.Bot = Bot;