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/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "chat-platform",
3
+ "version": "1.2.3",
4
+ "description": "Universal Chat Platform",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "test": "jest",
8
+ "test-ci": "jest --watch",
9
+ "lint": "eslint ./*.js ./helpers/*.js"
10
+ },
11
+ "author": "",
12
+ "license": "ISC",
13
+ "dependencies": {
14
+ "cli-color": "^1.4.0",
15
+ "cli-table": "^0.3.1",
16
+ "moment": "^2.24.0",
17
+ "np": "^5.0.2",
18
+ "npm": "^6.9.0",
19
+ "prettyjson": "^1.2.1",
20
+ "request": "^2.88.0",
21
+ "sequelize": "^5.21.6",
22
+ "sqlite3": "^4.1.1",
23
+ "underscore": "^1.9.1",
24
+ "underscore.string": "^3.3.5"
25
+ },
26
+ "devDependencies": {
27
+ "chai": "^4.1.1",
28
+ "chai-spies": "^1.0.0",
29
+ "eslint": "^4.12.1",
30
+ "jest": "^22.0.6"
31
+ }
32
+ }
@@ -0,0 +1,170 @@
1
+ const _ = require('underscore');
2
+ const _store = {};
3
+ const _storeUserIds = {};
4
+
5
+ const isEmpty = value => value == null || value === '';
6
+
7
+ function MemoryStore(chatId, userId, statics = {}, warnings = false) {
8
+ this.chatId = chatId != null ? String(chatId) : null;
9
+ this.userId = userId != null ? String(userId) : null;
10
+ // make sure userId is always a string
11
+ this.statics = Object.assign({}, statics, { userId: statics.userId != null ? String(statics.userId) : undefined });
12
+ if (warnings && _.isEmpty(statics)) {
13
+ console.trace('Warning: empty statics vars')
14
+ }
15
+ return this;
16
+ }
17
+ _.extend(MemoryStore.prototype, {
18
+ getPayload() {
19
+ // always precedence to userId
20
+ if (this.userId != null && _storeUserIds[this.userId] != null) {
21
+ return _storeUserIds[this.userId];
22
+ } else if (this.chatId != null && _store[this.chatId] != null) {
23
+ return _store[this.chatId];
24
+ } else if (this.userId != null) {
25
+ _storeUserIds[this.userId] = {};
26
+ return _storeUserIds[this.userId];
27
+ } else if (this.chatId != null) {
28
+ _store[this.chatId] = {};
29
+ return _store[this.chatId];
30
+ }
31
+ return {};
32
+ },
33
+ get(key) {
34
+ const keys = Array.from(arguments);
35
+ const payload = this.getPayload();
36
+
37
+ if (keys.length === 1) {
38
+ if (this.statics[keys[0]] != null) {
39
+ return this.statics[keys[0]];
40
+ } else {
41
+ return payload[key] != null ? payload[key] : null;
42
+ }
43
+ }
44
+ const result = {};
45
+ keys.forEach(key => {
46
+ if (this.statics[key] != null) {
47
+ result[key] = this.statics[key];
48
+ } else {
49
+ result[key] = payload[key];
50
+ }
51
+ });
52
+ return result;
53
+ },
54
+ remove() {
55
+ const keys = Array.from(arguments);
56
+ const payload = this.getPayload();
57
+ keys.forEach(key => {
58
+ // eslint-disable-next-line prefer-reflect
59
+ delete payload[key];
60
+ });
61
+ return this;
62
+ },
63
+ set(key, value) {
64
+ let payload = this.getPayload();
65
+ const staticKeys = Object.keys(this.statics);
66
+ if (_.isString(key) && staticKeys.includes(key)) {
67
+ console.log(`Warning: try to set a static key: ${key}`);
68
+ } else if (_.isObject(key) && _.intersection(staticKeys, Object.keys(key)).length !== 0) {
69
+ console.log(`Warning: try to set a static keys: ${_.intersection(staticKeys, Object.keys(key)).join(', ')}`);
70
+ }
71
+ // store values, skipping static keys
72
+ if (_.isString(key) && !staticKeys.includes(key)) {
73
+ payload[key] = value;
74
+ } else if (_.isObject(key)) {
75
+ payload = { ...payload, ..._.omit(key, staticKeys) };
76
+ }
77
+ // store the payload back
78
+ if (this.userId != null) {
79
+ _storeUserIds[this.userId] = payload;
80
+ } else if (this.chatId != null) {
81
+ _store[this.chatId] = payload;
82
+ }
83
+ return this;
84
+ },
85
+ dump() {
86
+ const payload = this.getPayload();
87
+ // eslint-disable-next-line no-console
88
+ console.log(payload);
89
+ },
90
+ all() {
91
+ const payload = this.getPayload();
92
+ return payload;
93
+ },
94
+ clear() {
95
+ if (this.userId != null) {
96
+ _storeUserIds[this.userId] = {};
97
+ _store[this.chatId] = null;
98
+ } else if (this.chatId != null) {
99
+ _store[this.chatId] = {};
100
+ _storeUserIds[this.userId] = null;
101
+ }
102
+ return this;
103
+ }
104
+ });
105
+
106
+ function MemoryFactory() {
107
+
108
+ this.getOrCreate = function(chatId, userId, statics) {
109
+ if (isEmpty(chatId) && isEmpty(userId)) {
110
+ return null;
111
+ }
112
+ // just create an class that just wraps chatId and userId, add static value (cline)
113
+ const store = new MemoryStore(chatId, userId, { ...statics });
114
+ return store;
115
+ /*const chatContext = this.get(chatId, userId);
116
+ if (chatContext == null) {
117
+ const memoryStore = new MemoryStore({ ...defaults });
118
+ _store[chatId] = memoryStore;
119
+ if (!isEmpty(userId)) {
120
+ _storeUserIds[userId] = memoryStore;
121
+ }
122
+ return _store[chatId];
123
+ }
124
+ return chatContext;
125
+ */
126
+ };
127
+ this.get = function(chatId, userId, statics) {
128
+ /*if (!isEmpty(chatId) && _store[chatId] != null) {
129
+ return _store[chatId];
130
+ } else if (!isEmpty(userId) && _storeUserIds[userId] != null) {
131
+ return _storeUserIds[userId];
132
+ }
133
+ return null;*/
134
+ return new MemoryStore(chatId, userId, { ...statics });
135
+ };
136
+
137
+ return this;
138
+ }
139
+ _.extend(MemoryFactory.prototype, {
140
+ name: 'Memory',
141
+ description: 'Memory context provider, it\' fast and synchronous but it doesn\'t persists the values, once the'
142
+ + ' server is restarted all contexts are lost. It doesn\'t requires any parameters. Good for testing.',
143
+ get: function(/*chatId, userId*/) {
144
+ },
145
+ getOrCreate: function(/*chatId, userId, statics*/) {
146
+ },
147
+ assignToUser(userId, context) {
148
+ // when merging a user into another, this trasnfer the current context to another user
149
+ // TODO perhaps remove other occurence
150
+ _storeUserIds[userId] = context;
151
+ },
152
+ reset() {
153
+ Object.keys(_store).forEach(key => delete _store[key]);
154
+ Object.keys(_storeUserIds).forEach(key => delete _storeUserIds[key]);
155
+ return this;
156
+ },
157
+ stop: function() {
158
+ return new Promise(function(resolve) {
159
+ resolve();
160
+ });
161
+ },
162
+ start: function() {
163
+ return new Promise(function(resolve) {
164
+ resolve();
165
+ });
166
+ }
167
+ });
168
+
169
+
170
+ module.exports = MemoryFactory;
@@ -0,0 +1,345 @@
1
+ const _ = require('underscore');
2
+ const fs = require('fs');
3
+ const moment = require('moment');
4
+ const crypto = require('crypto');
5
+ const lcd = require('../helpers/lcd');
6
+ const FileQueue = require('../helpers/promises-queue');
7
+ const filesQueue = {};
8
+
9
+ // memory cache, each loaded store is here and it's saved to filesystem at every changes,
10
+ // subsequent read hit the cache
11
+ let _store = {};
12
+ // main index
13
+ let _index;
14
+
15
+ const parse = content => {
16
+ let obj = null;
17
+ const date = new RegExp('^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{1,2}:[0-9]{1,2}:[0-9]{1,2}\.[0-9]{1,3}Z$');
18
+
19
+ obj = JSON.parse(content);
20
+ // todo fix with forEach
21
+ // go through every key/value to search for a date-like string
22
+ _(obj).each(function(value, key) {
23
+ if (_.isString(value) && value.match(date)) {
24
+ obj[key] = moment(value);
25
+ }
26
+ });
27
+
28
+ return obj;
29
+ };
30
+
31
+
32
+ const deleteFile = file => {
33
+ return new Promise((resolve, reject) => {
34
+ fs.unlink(file, err => {
35
+ if (err) {
36
+ reject(err)
37
+ } else {
38
+ resolve(true);
39
+ }
40
+ });
41
+ });
42
+ };
43
+
44
+ const exists = (file) => {
45
+ return new Promise(resolve => {
46
+ fs.exists(file, exists => resolve(exists));
47
+ });
48
+ };
49
+
50
+ const writeJson = (file, obj) => {
51
+
52
+ const serialized = JSON.stringify(obj);
53
+ return new Promise((resolve, reject) => {
54
+ fs.writeFile(file, serialized, err => {
55
+ if (err != null) {
56
+ reject(err);
57
+ } else {
58
+ resolve(obj);
59
+ }
60
+ });
61
+ });
62
+ };
63
+
64
+ const loadJson = file => {
65
+ return new Promise((resolve, reject) => {
66
+ fs.readFile(file, (err, content) => {
67
+
68
+ if (err != null) {
69
+ reject(err);
70
+ } else {
71
+ const index = parse(String(content));
72
+
73
+ if (index != null) {
74
+ resolve(index);
75
+ } else {
76
+ reject(new Error(`Unable to parse file ${file}`))
77
+ }
78
+ }
79
+ });
80
+ });
81
+ };
82
+
83
+
84
+ const loadOrCreateIndex = async ({ path }) => {
85
+ const indexPath = `${path}/index.json`;
86
+ const indexExists = await exists(indexPath);
87
+ if (!indexExists) {
88
+ let _index = { chatId: {}, userId: {} };
89
+ return writeJson(indexPath, _index);
90
+ } else {
91
+ return loadJson(indexPath);
92
+ }
93
+ };
94
+
95
+ const saveIndex = ({ path }) => {
96
+ const indexPath = `${path}/index.json`;
97
+ return writeJson(indexPath, _index);
98
+ };
99
+
100
+
101
+ const getOrLoadOrCreateIndex = async ({ path }) => {
102
+ if (_index != null) {
103
+ return _index;
104
+ } else {
105
+ _index = await loadOrCreateIndex({ path })
106
+ return _index;
107
+ }
108
+ };
109
+
110
+ /*const loadFileStore = (path, file, statics) => {
111
+ if (_store[file] != null) {
112
+ return Promise.resolve(_store[file]);
113
+ }
114
+ const store = new FileStore(null, `${path}/${file}`, statics);
115
+ return store.load()
116
+ .then(() => store);
117
+ }*/
118
+
119
+ const generateFileName = function(chatId, userId) {
120
+ return 'store-' + crypto.createHash('md5').update(`${chatId}${userId}`).digest('hex') + '.json';
121
+ };
122
+
123
+
124
+ function FileFactory(params) {
125
+
126
+ params = params || {};
127
+ if (_.isEmpty(params.path)) {
128
+ throw 'Plain file context provider: missing parameter "path"';
129
+ }
130
+ if (!fs.existsSync(params.path)) {
131
+ throw 'Plain file context provider: "path" (' + params.path + ') doesn\'t exist';
132
+ }
133
+
134
+ this.getOrCreate = function(chatId, userId = null, statics = {}) {
135
+ const { path } = params;
136
+ return new FileStore(chatId, userId, { ...statics }, { path });
137
+ };
138
+
139
+ this.get = function(chatId, userId, statics = {}) {
140
+ return this.getOrCreate(chatId, userId, statics);
141
+ };
142
+
143
+ return this;
144
+ }
145
+
146
+ _.extend(FileFactory.prototype, {
147
+ name: 'Plain File',
148
+ description: 'Simple file context provider: chat context will be stored in plain json files. Specify the storage path in'
149
+ + ' params as JSON config like this <pre style="margin-top: 10px;">\n'
150
+ + '{\n'
151
+ + '"path": "/my-path/my-context-files"\n'
152
+ +'}</pre>',
153
+ get(/*chatId*/) { },
154
+ getOrCreate(/*chatId, defaults*/) { },
155
+ start() {
156
+ return new Promise(function(resolve) {
157
+ resolve();
158
+ });
159
+ },
160
+ reset({ path }) {
161
+ const files = fs.readdirSync(path);
162
+ files.forEach(file => fs.unlinkSync(`${path}/${file}`));
163
+ //_store = {};
164
+ _index = null;
165
+ },
166
+ stop: function() {
167
+ return new Promise(function(resolve) {
168
+ resolve();
169
+ });
170
+ }
171
+ });
172
+
173
+ //function FileStore(defaults, file, statics = {}) {
174
+ function FileStore(chatId, userId, statics = {}, params, warnings = false) {
175
+ this.userId = userId;
176
+ this.chatId = chatId;
177
+ this.params = params;
178
+ // make sure userId is always a string
179
+ this.statics = Object.assign({}, statics, { userId: statics.userId != null ? String(statics.userId) : undefined });
180
+ if (warnings && _.isEmpty(statics)) {
181
+ console.trace('Warning: empty statics vars')
182
+ }
183
+ return this;
184
+ }
185
+ _.extend(FileStore.prototype, {
186
+ async get(key) {
187
+ const keys = Array.from(arguments);
188
+ const payload = await this.getPayload();
189
+ if (keys.length === 1) {
190
+ if (this.statics[keys[0]] != null) {
191
+ return this.statics[keys[0]];
192
+ } else {
193
+ return payload[key] != null ? payload[key] : undefined;
194
+ }
195
+ }
196
+ const result = {};
197
+ keys.forEach(key => {
198
+ if (this.statics[key] != null) {
199
+ result[key] = this.statics[key];
200
+ } else {
201
+ result[key] = payload[key];
202
+ }
203
+ });
204
+ return result;
205
+ },
206
+
207
+ parse(content) {
208
+ let obj = null;
209
+ let date = new RegExp('^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{1,2}:[0-9]{1,2}:[0-9]{1,2}\.[0-9]{1,3}Z$');
210
+ try {
211
+ obj = JSON.parse(content);
212
+ // go through every key/value to search for a date-like string
213
+ _(obj).each((value, key) => {
214
+ if (_.isString(value) && value.match(date)) {
215
+ obj[key] = moment(value);
216
+ }
217
+ });
218
+ } catch(e) {
219
+ // eslint-disable-next-line no-console
220
+ console.log(lcd.error('Error parsing context file: ' + this._file));
221
+ throw e;
222
+ }
223
+ return obj;
224
+ },
225
+
226
+ async getPayload() {
227
+ // get the index from the memory cache or from file, if doesn't exist create it
228
+ const userId = this.userId;
229
+ const chatId = this.chatId;
230
+ const { path } = this.params;
231
+ const index = await getOrLoadOrCreateIndex({ path })
232
+
233
+ // if context is already loaded, then return. Context is flushed to disk at every
234
+ // write, so if it's in memory no need to reload
235
+ if (this._context != null) {
236
+ return this._context;
237
+ } else if (userId != null && index.userId[userId] != null) {
238
+ this._file = `${path}/${index.userId[userId]}`;
239
+ await this.load();
240
+ return this._context;
241
+ } else if (chatId != null && index.chatId[chatId] != null) {
242
+ this._file = `${path}/${index.chatId[chatId]}`;
243
+ await this.load();
244
+ return this._context;
245
+ } else {
246
+ // file store doesn't exist yet, create one
247
+ const fileName = generateFileName(chatId, userId);
248
+ this._file = `${path}/${fileName}`;
249
+ this._context = {};
250
+ // store in the index the file reference
251
+ if (chatId != null) {
252
+ index.chatId[chatId] = fileName;
253
+ }
254
+ if (userId != null) {
255
+ index.userId[userId] = fileName;
256
+ }
257
+ // save index
258
+ await this.save();
259
+ await saveIndex({ path });
260
+
261
+ return this._context;
262
+ }
263
+ },
264
+
265
+ load() {
266
+ return new Promise((resolve, reject) => {
267
+ fs.readFile(this._file, (err, content) => {
268
+ if (err != null) {
269
+ reject(err);
270
+ } else {
271
+ this._context = this.parse(content);
272
+ resolve();
273
+ }
274
+ });
275
+ });
276
+ },
277
+ save() {
278
+ // store the value, before the task is executed or can be overwritten, it's a snapshot
279
+ const serialized = JSON.stringify(_.clone(this._context));
280
+ let saveTask = (resolve, reject) => {
281
+ fs.writeFile(this._file, serialized, err => {
282
+ // put the object back, don't know what happens here but some key disapper
283
+ this._context = this.parse(serialized);
284
+ if (err != null) {
285
+ reject(err);
286
+ } else {
287
+ resolve();
288
+ }
289
+ });
290
+ };
291
+
292
+ if (filesQueue[this._file] == null) {
293
+ filesQueue[this._file] = new FileQueue();
294
+ }
295
+ // add to a queue to prevent concurrent writing
296
+ return filesQueue[this._file].add(saveTask);
297
+ },
298
+
299
+ async remove() {
300
+ const keys = Array.from(arguments);
301
+ const payload = await this.getPayload();
302
+ keys.forEach(key => {
303
+ // eslint-disable-next-line prefer-reflect
304
+ delete payload[key];
305
+ });
306
+ await this.save();
307
+ return this;
308
+ },
309
+
310
+ async set(key, value) {
311
+ let payload = await this.getPayload();
312
+ const staticKeys = Object.keys(this.statics);
313
+ if (_.isString(key) && staticKeys.includes(key)) {
314
+ console.log(`Warning: try to set a static key: ${key}`);
315
+ } else if (_.isObject(key) && _.intersection(staticKeys, Object.keys(key)).length !== 0) {
316
+ console.log(`Warning: try to set a static keys: ${_.intersection(staticKeys, Object.keys(key)).join(', ')}`);
317
+ }
318
+ // store values, skipping static keys
319
+ if (_.isString(key) && !staticKeys.includes(key)) {
320
+ payload[key] = value;
321
+ } else if (_.isObject(key)) {
322
+ Object.entries(key)
323
+ .forEach(([key, value]) => {
324
+ if (!staticKeys.includes(key)) {
325
+ payload[key] = value;
326
+ }
327
+ });
328
+ }
329
+ await this.save();
330
+ return this;
331
+ },
332
+ dump() {
333
+ // eslint-disable-next-line no-console
334
+ console.log(this._context);
335
+ },
336
+ all() {
337
+ return this._context;
338
+ },
339
+ clear() {
340
+ this._context = {};
341
+ return this.save();
342
+ }
343
+ });
344
+
345
+ module.exports = FileFactory;