Skip to content
This repository has been archived by the owner on Dec 26, 2021. It is now read-only.

Devel #5

Merged
merged 5 commits into from
Apr 19, 2017
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
667 changes: 11 additions & 656 deletions LICENCE.md

Large diffs are not rendered by default.

17 changes: 10 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,13 +108,13 @@ bot.hears(["keyword", "hello.*"], "Group")
## Node callback is supported

```javascript
bot.hear("hello.*", "Group", (message, error) => {
bot.hear("hello.*", "Group", (data, error) => {
console.log("Data:", JSON.stringify(data, null, 2));
});
```

```javascript
bot.hears(["keyword", "hello.*"], "Group", (message, error) => {
bot.hears(["keyword", "hello.*"], "Group", (data, error) => {
console.log("Data:", JSON.stringify(data, null, 2));
});
```
Expand All @@ -124,17 +124,17 @@ bot.hears(["keyword", "hello.*"], "Group", (message, error) => {
### A simple message

```javascript
bot.sendText("Hello world.", data.raw);
bot.sendText("Hello world.", data.message);
```

### A Video or Image message

```javascript
bot.sendImage("http://url-of-media", data.raw, optionalMeta);
bot.sendImage("http://url-of-media", data.message, optionalMeta);

// OR

bot.sendVideo("http://url-of-media", data.raw, optionalMeta);
bot.sendVideo("http://url-of-media", data.message, optionalMeta);
```

`optionalMeta` is an object of optional information for the media.
Expand All @@ -161,11 +161,14 @@ class FakeMiddleware {
return "FakeMiddleware";
}

receive(bot, message) {
incoming(bot, message) {
// the return value can be an Promise<object>, Observable<object> or null
return "hello world";
}

send(bot, message) {
outgoing(bot, message) {
// the return value can be an Promise<object>, Observable<object> or null
// The object.content field will be use to update the text of the message sent.
return "Good buy world";
}
}
Expand Down
155 changes: 128 additions & 27 deletions lib/core/bot.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,19 @@
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.receiveMiddlewares = [];
this.sendMiddlewares = [];
this.incomingMiddlewares = [];
this.outgoingMiddlewares = [];
const httpOptions = { host: '0.0.0.0', port: 8080 };
this.httpOptions = obj && obj.http || httpOptions;
this.httpEndpoints = [];
Expand All @@ -20,22 +24,28 @@ class Bot {
getHTTPEndpoints() {
return this.httpEndpoints;
}
use(instance) {
use(instance, filter) {
if (instance.listen) {
this.logger.info({ method: 'use', message: `Integration: ${instance.serviceName()}` });
this.addIntegration(instance);
}
else if (instance.receive || instance.send) {
if (instance.receive) {
this.logger
.info({ method: 'use', message: `Receive middleware: ${instance.serviceName()}` });
this.receiveMiddlewares.push(instance.receive);
}
if (instance.send) {
this.logger
.info({ method: 'use', message: `Send middleware: ${instance.serviceName()}` });
this.sendMiddlewares.push(instance.send);
}
else if (instance.incoming) {
this.logger
.info({ method: 'use', message: `incoming middleware: ${instance.serviceName()}` });
this.incomingMiddlewares.push({
name: `${instance.serviceName()}.incoming`,
middleware: instance,
filter: filter || null,
});
}
else if (instance.outgoing) {
this.logger
.info({ method: 'use', message: `outgoing middleware: ${instance.serviceName()}` });
this.outgoingMiddlewares.push({
name: `${instance.serviceName()}.outgoing`,
middleware: instance,
filter: filter || null,
});
}
return;
}
Expand All @@ -51,8 +61,9 @@ class Bot {
}
const listener = Rx_1.Observable
.merge(...R.flatten(R.map((integration) => [integration.connect(), integration.listen()], this.integrations)))
.mergeMap((message) => this.testIncoming(message, patternRegex, messageTypesArr)
? this.processIncomingMessage(message) : Rx_1.Observable.empty());
.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) {
Expand All @@ -61,9 +72,10 @@ class Bot {
const patternRegexes = R.map((pattern) => new RegExp(pattern, 'ig'), patterns);
const listener = Rx_1.Observable.merge(...R.map((integration) => integration.listen(), this.integrations))
.mergeMap((message) => {
const matches = R.pipe(R.map((patternRegex) => this.testIncoming(message, patternRegex, messageTypesArr)), R.reject(R.equals(false)));
const messageUpdated = this.processIncomingMessage(message);
const matches = R.pipe(R.map((patternRegex) => this.testIncoming(messageUpdated.message, patternRegex, messageTypesArr)), R.reject(R.equals(false)));
if (!R.isEmpty(matches(patternRegexes))) {
return this.processIncomingMessage(message);
return Promise.resolve(messageUpdated);
}
return Rx_1.Observable.empty();
});
Expand All @@ -73,8 +85,9 @@ class Bot {
return this.hear(true, messageTypes, cb);
}
sendText(text, message) {
return this.processOutcomingMessage(text, message)
.then((textUpdated) => {
return this.processOutgoingContent(text, message)
.then((updated) => {
const content = updated.content || text;
let data = {
'@context': 'https://www.w3.org/ns/activitystreams',
'generator': {
Expand All @@ -83,7 +96,7 @@ class Bot {
type: 'Service',
},
'object': {
content: textUpdated,
content: content,
type: 'Note',
},
'to': {
Expand All @@ -102,6 +115,17 @@ class Bot {
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) {
Expand Down Expand Up @@ -131,7 +155,7 @@ class Bot {
testIncoming(message, patternRegex, messageTypesArr) {
const messageContext = R.prop('@context', message);
if (!messageContext) {
this.logger.debug('Message received should follow Broid schema.', message);
this.logger.debug('Message incoming should follow Broid schema.', message);
return false;
}
const content = R.path(['object', 'content'], message);
Expand Down Expand Up @@ -165,7 +189,7 @@ class Bot {
return Promise.reject('Message should follow broid-schemas.');
}
sendMedia(url, mediaType, message, meta) {
return this.processOutcomingMessage(url, message)
return this.processOutgoingContent(url, message)
.then((urlUpdated) => {
let data = {
'@context': 'https://www.w3.org/ns/activitystreams',
Expand Down Expand Up @@ -196,19 +220,96 @@ class Bot {
if (router) {
if (!this.express) {
this.express = express();
this.express.use(bodyParser.json());
this.express.use(bodyParser.urlencoded({ extended: false }));
}
const httpPath = `/webhook/${integration.serviceName()}`;
this.httpEndpoints.push(httpPath);
this.express.use(httpPath, router);
}
return;
}
chain(input, filters) {
let 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) {
return Promise.reduce(this.receiveMiddlewares, (data, fn) => fn(this, data), message)
.then((data) => ({ message: data, raw: 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 }));
}
processOutcomingMessage(messageText, message) {
return Promise.reduce(this.sendMiddlewares, (text, fn) => fn(this, text, message), messageText);
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((data_) => {
let data = data_;
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() {
if (this.express && !this.httpServer) {
Expand Down
17 changes: 11 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
{
"name": "@broid/kit",
"version": "0.0.1",
"version": "0.1.0",
"main": "lib/core/index.js",
"license": "AGPL-3.0+",
"license": "Apache-2.0",
"licenses": [
{
"type": "Apache-2.0",
"url": "http://www.apache.org/licenses/LICENSE-2.0"
}
],
"author": "Broid Team <opensource@broid.ai> (https://broid.ai)",
"description": "Bot framework supported all messaging plateforms and middlewares.",
"repository": {
Expand Down Expand Up @@ -39,8 +45,6 @@
"watch": "concurrently --kill-others \"npm run lint:watch\" \"npm run tsc:watch\""
},
"devDependencies": {
"@types/bluebird": "^3.0.37",
"@types/bluebird-global": "^3.0.1",
"@types/node": "^7.0.5",
"@types/ramda": "0.0.3",
"ava": "^0.18.1",
Expand All @@ -51,14 +55,15 @@
"sinon": "^1.17.7",
"tslint": "^4.3.1",
"tslint-eslint-rules": "^4.0.0",
"tslint-microsoft-contrib": "^4.0.1",
"typescript": "~2.2.2",
"watch": "^1.0.1",
"tslint-microsoft-contrib": "^4.0.1"
"watch": "^1.0.1"
},
"dependencies": {
"@broid/schemas": "^1.0.0",
"@broid/utils": "^1.1.0",
"bluebird": "^3.5.0",
"body-parser": "^1.17.1",
"express": "^4.15.2",
"ramda": "^0.23.0",
"rxjs": "^5.2.0"
Expand Down
Loading