chat-platform 1.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc +17 -0
- package/__tests__/chat-platform.js +223 -0
- package/__tests__/context-provider-memory.js +222 -0
- package/__tests__/context-provider-plain-file.js +174 -0
- package/__tests__/context-provider-sqlite.js +239 -0
- package/__tests__/dummy/audio.mp3 +0 -0
- package/__tests__/dummy/file.bin +0 -0
- package/__tests__/dummy/file.mp4 +0 -0
- package/__tests__/dummy/file.pdf +0 -0
- package/__tests__/dummy/image.png +0 -0
- package/__tests__/dummy/mission-control.backup +0 -0
- package/__tests__/dummy/video.mov +0 -0
- package/__tests__/universal-platform.js +190 -0
- package/blank/empty.sqlite +0 -0
- package/chat-context-factory.js +92 -0
- package/chat-log.js +115 -0
- package/chat-platform.js +1276 -0
- package/helpers/lcd.js +120 -0
- package/helpers/promises-queue.js +41 -0
- package/helpers/utils.js +25 -0
- package/index.js +6 -0
- package/jest.config.js +5 -0
- package/lib/lcd.js +151 -0
- package/lib/red-stub.js +212 -0
- package/lib/utils.js +29 -0
- package/model.nlp +688 -0
- package/package.json +32 -0
- package/providers/memory.js +170 -0
- package/providers/plain-file.js +345 -0
- package/providers/sqlite.js +267 -0
- package/universal.js +67 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
const _ = require('underscore');
|
|
2
|
+
|
|
3
|
+
const _storeUserIds = {};
|
|
4
|
+
const Sequelize = require('sequelize');
|
|
5
|
+
const { QueryTypes } = require('sequelize');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const lcd = require('../lib/lcd');
|
|
8
|
+
|
|
9
|
+
const Op = Sequelize.Op;
|
|
10
|
+
|
|
11
|
+
const isEmpty = value => value == null || value === '';
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
function SQLiteStore(chatId, userId, statics = {}, warnings = false) {
|
|
17
|
+
this.chatId = chatId != null ? String(chatId) : null;
|
|
18
|
+
this.userId = userId != null ? String(userId) : null;
|
|
19
|
+
// make sure userId is always a string
|
|
20
|
+
this.statics = Object.assign({}, statics, { userId: statics.userId != null ? String(statics.userId) : undefined });
|
|
21
|
+
if (warnings && _.isEmpty(statics)) {
|
|
22
|
+
console.trace('Warning: empty statics vars')
|
|
23
|
+
}
|
|
24
|
+
return this;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function SQLiteFactory(params) {
|
|
28
|
+
params = params || {};
|
|
29
|
+
let fileCreatedAutomatically = false;
|
|
30
|
+
if (_.isEmpty(params.dbPath)) {
|
|
31
|
+
throw 'SQLite context provider: missing parameter "dbPath"';
|
|
32
|
+
}
|
|
33
|
+
if (!fs.existsSync(params.dbPath)) {
|
|
34
|
+
//throw 'SQLite context provider: "dbPath" (' + params.path + ') doesn\'t exist';
|
|
35
|
+
try {
|
|
36
|
+
fs.copyFileSync(`${__dirname}/../blank/empty.sqlite`, params.dbPath);
|
|
37
|
+
fileCreatedAutomatically = true;
|
|
38
|
+
} catch(e) {
|
|
39
|
+
throw 'SQLite context provider: "dbPath" (' + params.path + ') doesn\'t exist and unable to create';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const sequelize = new Sequelize('mission_control', '', '', {
|
|
44
|
+
host: 'localhost',
|
|
45
|
+
dialect: 'sqlite',
|
|
46
|
+
storage: params.dbPath,
|
|
47
|
+
logging: false
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const Context = sequelize.define('context', {
|
|
51
|
+
userId: { type: Sequelize.STRING },
|
|
52
|
+
chatId: { type: Sequelize.STRING },
|
|
53
|
+
payload: { type: Sequelize.STRING }
|
|
54
|
+
}, {
|
|
55
|
+
indexes: [
|
|
56
|
+
{ name: 'chatid_userid', using: 'BTREE', fields: ['userId'] },
|
|
57
|
+
{ name: 'chatid_chatid', using: 'BTREE', fields: ['chatId'] }
|
|
58
|
+
]
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
// **
|
|
64
|
+
// Start definition if SQLite store, with closure I can spare passing a Context as configuration
|
|
65
|
+
// fo the class
|
|
66
|
+
// **
|
|
67
|
+
_.extend(SQLiteStore.prototype, {
|
|
68
|
+
async get(key) {
|
|
69
|
+
const keys = Array.from(arguments);
|
|
70
|
+
const { payload } = await this.getPayload();
|
|
71
|
+
if (keys.length === 1) {
|
|
72
|
+
if (this.statics[keys[0]] != null) {
|
|
73
|
+
return this.statics[keys[0]];
|
|
74
|
+
} else {
|
|
75
|
+
return payload[key] != null ? payload[key] : null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const result = {};
|
|
79
|
+
keys.forEach(key => {
|
|
80
|
+
if (this.statics[key] != null) {
|
|
81
|
+
result[key] = this.statics[key];
|
|
82
|
+
} else {
|
|
83
|
+
result[key] = payload[key];
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
return result;
|
|
87
|
+
},
|
|
88
|
+
async remove() {
|
|
89
|
+
const keys = Array.from(arguments);
|
|
90
|
+
const { id, payload } = await this.getPayload();
|
|
91
|
+
keys.forEach(key => {
|
|
92
|
+
// eslint-disable-next-line prefer-reflect
|
|
93
|
+
delete payload[key];
|
|
94
|
+
});
|
|
95
|
+
await Context.update({ payload: JSON.stringify(payload) }, { where: { id }});
|
|
96
|
+
return this;
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
async getPayload() {
|
|
100
|
+
// get payload using chatId or userId
|
|
101
|
+
const contexts = await Context.findAll({ where: {
|
|
102
|
+
[Op.or]: [
|
|
103
|
+
{ chatId: this.chatId },
|
|
104
|
+
{ userId: this.userId }
|
|
105
|
+
]
|
|
106
|
+
}});
|
|
107
|
+
let payload;
|
|
108
|
+
let context;
|
|
109
|
+
if (contexts.length === 0) {
|
|
110
|
+
// if not present then create the row
|
|
111
|
+
context = await Context.create({ payload: JSON.stringify({}), chatId: this.chatId, userId: this.userId });
|
|
112
|
+
payload = {};
|
|
113
|
+
} else {
|
|
114
|
+
// if by any change there are two matched rows, one for the chatId and one for userId
|
|
115
|
+
// always prefer the userId (that could happen if the user star using the sqlite provider) as
|
|
116
|
+
// is and at some point the MC_store assign the context to the user
|
|
117
|
+
context = contexts.find(context => context.userId === this.userId);
|
|
118
|
+
if (context == null) {
|
|
119
|
+
context = contexts.find(context => context.chatId === this.chatId);
|
|
120
|
+
}
|
|
121
|
+
if (context == null) {
|
|
122
|
+
context = contexts[0];
|
|
123
|
+
}
|
|
124
|
+
// finally decode
|
|
125
|
+
try {
|
|
126
|
+
payload = JSON.parse(context.payload);
|
|
127
|
+
} catch(e) {
|
|
128
|
+
// default if error
|
|
129
|
+
payload = {};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return { payload, id: context.id };
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
async set(key, value) {
|
|
136
|
+
let { id, payload } = await this.getPayload();
|
|
137
|
+
const staticKeys = Object.keys(this.statics);
|
|
138
|
+
if (_.isString(key) && staticKeys.includes(key)) {
|
|
139
|
+
console.log(`Warning: try to set a static key: ${key}`);
|
|
140
|
+
} else if (_.isObject(key) && _.intersection(staticKeys, Object.keys(key)).length !== 0) {
|
|
141
|
+
console.log(`Warning: try to set a static keys: ${_.intersection(staticKeys, Object.keys(key)).join(', ')}`);
|
|
142
|
+
}
|
|
143
|
+
// store values, skipping static keys
|
|
144
|
+
if (_.isString(key) && !staticKeys.includes(key)) {
|
|
145
|
+
payload[key] = value;
|
|
146
|
+
} else if (_.isObject(key)) {
|
|
147
|
+
payload = { ...payload, ..._.omit(key, staticKeys) };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
await Context.update({ payload: JSON.stringify(payload) }, { where: { id }});
|
|
151
|
+
return this;
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
async dump() {
|
|
155
|
+
const { payload } = await this.getPayload();
|
|
156
|
+
// eslint-disable-next-line no-console
|
|
157
|
+
console.log(payload);
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
async all() {
|
|
161
|
+
const { payload } = await this.getPayload();
|
|
162
|
+
return payload;
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
async clear() {
|
|
166
|
+
const { id } = await this.getPayload();
|
|
167
|
+
await Context.update({ payload: JSON.stringify({}) }, { where: { id }})
|
|
168
|
+
return this;
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
// **
|
|
172
|
+
// End definition if SQLite store
|
|
173
|
+
// **
|
|
174
|
+
|
|
175
|
+
this.getOrCreate = async function(chatId, userId, statics) {
|
|
176
|
+
if (isEmpty(chatId) && isEmpty(userId)) {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
// just create an class that just wraps chatId and userId, add static value (cline)
|
|
180
|
+
const store = new SQLiteStore(chatId, userId, { ...statics });
|
|
181
|
+
return store;
|
|
182
|
+
};
|
|
183
|
+
this.get = function(chatId, userId, statics) {
|
|
184
|
+
return new SQLiteStore(chatId, userId, statics);
|
|
185
|
+
};
|
|
186
|
+
this.assignToUser = async (userId, context) => {
|
|
187
|
+
};
|
|
188
|
+
this.reset = async () => {
|
|
189
|
+
await Context.destroy({ where: {} });
|
|
190
|
+
};
|
|
191
|
+
this.drop = async () => {
|
|
192
|
+
await sequelize.query(
|
|
193
|
+
`DROP TABLE "contexts";
|
|
194
|
+
DROP INDEX "chatid_userid";
|
|
195
|
+
DROP INDEX "chatid_chatid";`,
|
|
196
|
+
{ type: QueryTypes.SELECT }
|
|
197
|
+
);
|
|
198
|
+
};
|
|
199
|
+
this.start = async () => {
|
|
200
|
+
/*
|
|
201
|
+
To test dropping the table
|
|
202
|
+
DROP TABLE "contexts";
|
|
203
|
+
DROP INDEX "chatid_userid";
|
|
204
|
+
DROP INDEX "chatid_chatid";
|
|
205
|
+
*/
|
|
206
|
+
// if table doesn't exists, then create
|
|
207
|
+
const tableExists = await sequelize.query(
|
|
208
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='contexts';",
|
|
209
|
+
{ type: QueryTypes.SELECT }
|
|
210
|
+
);
|
|
211
|
+
const createTable = tableExists.length === 0;
|
|
212
|
+
// create the table
|
|
213
|
+
try {
|
|
214
|
+
if (createTable) {
|
|
215
|
+
await Context.sync();
|
|
216
|
+
}
|
|
217
|
+
// then log, don't move, keep the log lines together
|
|
218
|
+
console.log(lcd.timestamp() + 'SQLite context provider configuration:');
|
|
219
|
+
console.log(lcd.timestamp() + ' ' + lcd.green('dbPath: ') + lcd.grey(params.dbPath)
|
|
220
|
+
+ (fileCreatedAutomatically ? ' - file was missing, empty one created automatically ' : ''));
|
|
221
|
+
if (createTable) {
|
|
222
|
+
console.log(lcd.timestamp() + ' ' + lcd.green('database: ') + lcd.grey('table missing, created successfully'));
|
|
223
|
+
} else {
|
|
224
|
+
console.log(lcd.timestamp() + ' ' + lcd.green('database: ') + lcd.grey('OK'));
|
|
225
|
+
}
|
|
226
|
+
} catch(e) {
|
|
227
|
+
lcd.dump(e, 'Something went wrong creating the SQLite "contexts" table');
|
|
228
|
+
throw e;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return true;
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
return this;
|
|
235
|
+
}
|
|
236
|
+
_.extend(SQLiteFactory.prototype, {
|
|
237
|
+
name: 'SQLite',
|
|
238
|
+
description: 'SQLite context provider: chat context will be stored a SQLite file. Specify the path of *.sqlite file in the'
|
|
239
|
+
+ ' JSON config like this <pre style="margin-top: 10px;">\n'
|
|
240
|
+
+ '{\n'
|
|
241
|
+
+ '"dbPath": "/my-path/my-database.sqlite"\n'
|
|
242
|
+
+ '}</pre>'
|
|
243
|
+
+ '<br/> The table <em>context</em> will be automatically created.',
|
|
244
|
+
get: function(/*chatId, userId*/) {
|
|
245
|
+
},
|
|
246
|
+
getOrCreate: function(/*chatId, userId, defaults*/) {
|
|
247
|
+
},
|
|
248
|
+
assignToUser(userId, context) {
|
|
249
|
+
// when merging a user into another, this trasnfer the current context to another user
|
|
250
|
+
},
|
|
251
|
+
reset() {
|
|
252
|
+
return this;
|
|
253
|
+
},
|
|
254
|
+
stop: function() {
|
|
255
|
+
return new Promise(function(resolve) {
|
|
256
|
+
resolve();
|
|
257
|
+
});
|
|
258
|
+
},
|
|
259
|
+
start: function() {
|
|
260
|
+
return new Promise(function(resolve) {
|
|
261
|
+
resolve();
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
module.exports = SQLiteFactory;
|
package/universal.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
const moment = require('moment');
|
|
2
|
+
const ChatExpress = require('./chat-platform');
|
|
3
|
+
const utils = require('./lib/utils');
|
|
4
|
+
const when = utils.when;
|
|
5
|
+
const _ = require('underscore');
|
|
6
|
+
|
|
7
|
+
const Universal = new ChatExpress({
|
|
8
|
+
transport: 'universal',
|
|
9
|
+
transportDescription: 'Universal Connector',
|
|
10
|
+
chatIdKey(payload) {
|
|
11
|
+
return payload.chatId;
|
|
12
|
+
},
|
|
13
|
+
userIdKey(payload) {
|
|
14
|
+
return payload.userId;
|
|
15
|
+
},
|
|
16
|
+
tsKey() {
|
|
17
|
+
return moment();
|
|
18
|
+
},
|
|
19
|
+
language() {
|
|
20
|
+
return null;
|
|
21
|
+
},
|
|
22
|
+
onStart() {
|
|
23
|
+
const options = this.getOptions();
|
|
24
|
+
return _.isFunction(options._onStart) ? when(options._onStart()) : when(true);
|
|
25
|
+
},
|
|
26
|
+
onStop() {
|
|
27
|
+
const options = this.getOptions();
|
|
28
|
+
return _.isFunction(options._onStop) ? when(options._onStop()) : when(true);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
Universal.mixin({
|
|
34
|
+
onStart(func) {
|
|
35
|
+
const options = this.getOptions();
|
|
36
|
+
options._onStart = func.bind(this);
|
|
37
|
+
return this;
|
|
38
|
+
},
|
|
39
|
+
onStop(func) {
|
|
40
|
+
const options = this.getOptions();
|
|
41
|
+
options._onStop = func.bind(this);
|
|
42
|
+
return this;
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
ChatExpress.registerParam(
|
|
47
|
+
'messageFlag',
|
|
48
|
+
'select',
|
|
49
|
+
{
|
|
50
|
+
label: 'Flag message',
|
|
51
|
+
default: 'new',
|
|
52
|
+
description: 'Flag the message with a label and store on Mission Control',
|
|
53
|
+
placeholder: 'Select flag',
|
|
54
|
+
options: [
|
|
55
|
+
{ value: 'answer', label: 'Answer' },
|
|
56
|
+
{ value: 'default', label: 'Default' },
|
|
57
|
+
{ value: 'error', label: 'Error' },
|
|
58
|
+
{ value: 'info', label: 'Info' },
|
|
59
|
+
{ value: 'new', label: 'New'},
|
|
60
|
+
{ value: 'not_understood', label: 'Not understood' },
|
|
61
|
+
{ value: 'question', label: 'Question' }
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
module.exports = Universal;
|