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.
@@ -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;