@tiledesk/tiledesk-server 2.10.100 → 2.10.101

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/CHANGELOG.md CHANGED
@@ -5,6 +5,13 @@
5
5
  🚀 IN PRODUCTION 🚀
6
6
  (https://www.npmjs.com/package/@tiledesk/tiledesk-server/v/2.3.77)
7
7
 
8
+ # 2.10.101
9
+ - Update: messenger-connector to 0.1.27
10
+ - Update: multi-worker to 0.3.3
11
+ - Added: endpoints for unanswered questions
12
+ - Update: endppoints to static logs
13
+ - Improved knowledge base import/export
14
+
8
15
  # 2.10.100
9
16
  - Update: removed verbose logs
10
17
 
package/app.js CHANGED
@@ -127,6 +127,7 @@ var quotes = require('./routes/quotes');
127
127
  var integration = require('./routes/integration')
128
128
  var kbsettings = require('./routes/kbsettings');
129
129
  var kb = require('./routes/kb');
130
+ var unanswered = require('./routes/unanswered');
130
131
 
131
132
  // var admin = require('./routes/admin');
132
133
  var faqpub = require('./routes/faqpub');
@@ -614,6 +615,7 @@ app.use('/:projectid/quotes', [passport.authenticate(['basic', 'jwt'], { session
614
615
  app.use('/:projectid/integration', [passport.authenticate(['basic', 'jwt'], { session: false }), validtoken, roleChecker.hasRoleOrTypes('admin', ['bot','subscription'])], integration )
615
616
 
616
617
  app.use('/:projectid/kbsettings', [passport.authenticate(['basic', 'jwt'], { session: false }), validtoken, roleChecker.hasRoleOrTypes('agent', ['bot','subscription'])], kbsettings);
618
+ app.use('/:projectid/kb/unanswered', [passport.authenticate(['basic', 'jwt'], { session: false }), validtoken, roleChecker.hasRoleOrTypes('admin', ['bot','subscription'])], unanswered);
617
619
  app.use('/:projectid/kb', [passport.authenticate(['basic', 'jwt'], { session: false }), validtoken, roleChecker.hasRoleOrTypes('admin', ['bot','subscription'])], kb);
618
620
 
619
621
  app.use('/:projectid/logs', [passport.authenticate(['basic', 'jwt'], { session: false }), validtoken, roleChecker.hasRole('admin')], logs);
@@ -155,6 +155,7 @@ class RoleChecker {
155
155
  }
156
156
  q.exec(function (err, project_user) {
157
157
  if (err) {
158
+ winston.error("Error on Request path: " + req.originalUrl);
158
159
  winston.error("Error getting project_user for hasrole",err);
159
160
  return next(err);
160
161
  }
@@ -29,6 +29,18 @@ const FlowLogsSchema = new Schema(
29
29
  type: String,
30
30
  required: false,
31
31
  },
32
+ webhook_id: {
33
+ type: String,
34
+ required: false,
35
+ },
36
+ shortExp: {
37
+ type: Date,
38
+ required: false
39
+ },
40
+ longExp: {
41
+ type: Date,
42
+ required: false
43
+ },
32
44
  level: {
33
45
  type: String,
34
46
  required: true,
@@ -43,6 +55,9 @@ const FlowLogsSchema = new Schema(
43
55
  );
44
56
 
45
57
  FlowLogsSchema.index({ request_id: 1 }, { unique: true });
58
+ FlowLogsSchema.index({ webhook_id: 1 });
59
+ FlowLogsSchema.index({ shortExp: 1 }, { expireAfterSeconds: 300 }); // 5 minutes
60
+ FlowLogsSchema.index({ longExp: 1 }, { expireAfterSeconds: 1800 }); // 30 minutes
46
61
 
47
62
  // FlowLogsSchema.pre('findOneAndUpdate', async function (next) {
48
63
  // const update = this.getUpdate();
@@ -1,6 +1,7 @@
1
1
  var mongoose = require('mongoose');
2
2
  var Schema = mongoose.Schema;
3
3
  var winston = require('../config/winston');
4
+ let expireAfterSeconds = process.env.UNANSWERED_QUESTION_EXPIRATION_TIME || 7 * 24 * 60 * 60; // 7 days
4
5
 
5
6
  var EngineSchema = new Schema({
6
7
  name: {
@@ -122,7 +123,35 @@ var KBSchema = new Schema({
122
123
  timestamps: true
123
124
  })
124
125
 
126
+ const UnansweredQuestionSchema = new Schema({
127
+ id_project: {
128
+ type: String,
129
+ required: true,
130
+ index: true
131
+ },
132
+ namespace: {
133
+ type: String,
134
+ required: true,
135
+ index: true
136
+ },
137
+ question: {
138
+ type: String,
139
+ required: true
140
+ },
141
+ created_at: {
142
+ type: Date,
143
+ default: Date.now
144
+ },
145
+ updated_at: {
146
+ type: Date,
147
+ default: Date.now
148
+ }
149
+ },{
150
+ timestamps: true
151
+ });
125
152
 
153
+ // Add TTL index to automatically delete documents after 30 days
154
+ UnansweredQuestionSchema.index({ created_at: 1 }, { expireAfterSeconds: expireAfterSeconds }); // 30 days
126
155
 
127
156
  // DEPRECATED !! - Start
128
157
  var KBSettingSchema = new Schema({
@@ -158,15 +187,13 @@ const KBSettings = mongoose.model('KBSettings', KBSettingSchema);
158
187
  const Engine = mongoose.model('Engine', EngineSchema)
159
188
  const Namespace = mongoose.model('Namespace', NamespaceSchema)
160
189
  const KB = mongoose.model('KB', KBSchema)
190
+ const UnansweredQuestion = mongoose.model('UnansweredQuestion', UnansweredQuestionSchema)
161
191
 
162
- // module.exports = {
163
- // KBSettings: KBSettings,
164
- // KB: KB
165
- // }
166
192
 
167
193
  module.exports = {
168
194
  KBSettings: KBSettings,
169
195
  Namespace: Namespace,
170
196
  Engine: Engine,
171
- KB: KB
197
+ KB: KB,
198
+ UnansweredQuestion: UnansweredQuestion
172
199
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tiledesk/tiledesk-server",
3
3
  "description": "The Tiledesk server module",
4
- "version": "2.10.100",
4
+ "version": "2.10.101",
5
5
  "scripts": {
6
6
  "start": "node ./bin/www",
7
7
  "pretest": "mongodb-runner start",
@@ -44,7 +44,7 @@
44
44
  "@tiledesk/tiledesk-dialogflow-connector": "^1.8.4",
45
45
  "@tiledesk/tiledesk-json-rules-engine": "^4.0.3",
46
46
  "@tiledesk/tiledesk-kaleyra-proxy": "^0.1.7",
47
- "@tiledesk/tiledesk-messenger-connector": "^0.1.24",
47
+ "@tiledesk/tiledesk-messenger-connector": "^0.1.27",
48
48
  "@tiledesk/tiledesk-rasa-connector": "^1.0.10",
49
49
  "@tiledesk/tiledesk-telegram-connector": "^0.1.14",
50
50
  "@tiledesk/tiledesk-tybot-connector": "^2.0.19",
@@ -53,7 +53,7 @@
53
53
  "@tiledesk/tiledesk-sms-connector": "^0.1.11",
54
54
  "@tiledesk/tiledesk-vxml-connector": "^0.1.76",
55
55
  "@tiledesk/tiledesk-voice-twilio-connector": "^0.1.22",
56
- "@tiledesk/tiledesk-multi-worker": "^0.3.2",
56
+ "@tiledesk/tiledesk-multi-worker": "^0.3.3",
57
57
  "passport-oauth2": "^1.8.0",
58
58
  "amqplib": "^0.5.5",
59
59
  "app-root-path": "^3.0.0",
package/routes/kb.js CHANGED
@@ -790,7 +790,7 @@ router.post('/namespace/import/:id', upload.single('uploadFile'), async (req, re
790
790
  });
791
791
  }
792
792
 
793
- let deleteResponse = await KB.deleteMany({ id_project: id_project, namespace: namespace_id }).catch((err) => {
793
+ let deleteResponse = await KB.deleteMany({ id_project: id_project, namespace: namespace_id, type: { $in: ['url', 'text', 'faq'] } }).catch((err) => {
794
794
  winston.error("deleteMany error: ", err);
795
795
  return res.status(500).send({ success: false, error: err });
796
796
  })
package/routes/logs.js CHANGED
@@ -87,35 +87,36 @@ router.post('/whatsapp', async (req, res) => {
87
87
  })
88
88
 
89
89
 
90
- router.get('/flows/:request_id', async (req, res) => {
90
+ router.get('/flows/:id', async (req, res) => {
91
+ const id = req.params.id;
92
+ const { timestamp, direction, logLevel, type } = req.query;
91
93
 
92
- let request_id = req.params.request_id;
93
- const { timestamp, direction, logLevel } = req.query;
94
-
95
- if (!request_id) {
96
- return res.status(400).send({ success: false, error: "Missing required parameter 'request_id'." });
94
+ if (!id) {
95
+ return res.status(400).send({ success: false, error: "Missing required parameter 'id'." });
97
96
  }
98
97
 
98
+ // Determine if we're searching by request_id or webhook_id
99
+ const isWebhook = type === 'webhook';
100
+ const queryField = isWebhook ? 'webhook_id' : 'request_id';
101
+
99
102
  let method;
100
103
 
101
104
  if (!timestamp) {
102
- method = logsService.getLastRows(request_id, 20, logLevel);
105
+ method = logsService.getLastRows(id, 20, logLevel, queryField);
103
106
  } else if (direction === 'prev') {
104
- logsService.get
105
- method = logsService.getOlderRows(request_id, 10, logLevel, new Date(timestamp));
107
+ method = logsService.getOlderRows(id, 10, logLevel, new Date(timestamp), queryField);
106
108
  } else if (direction === 'next') {
107
- method = logsService.getNewerRows(request_id, 10, logLevel, new Date(timestamp))
109
+ method = logsService.getNewerRows(id, 10, logLevel, new Date(timestamp), queryField);
108
110
  } else {
109
- return res.status(400).send({ success: false, error: "Missing or invalid 'direction' parameter. Use 'prev' or 'next'."})
111
+ return res.status(400).send({ success: false, error: "Missing or invalid 'direction' parameter. Use 'prev' or 'next'."});
110
112
  }
111
113
 
112
114
  method.then((logs) => {
113
115
  res.status(200).send(logs);
114
116
  }).catch((err) => {
115
117
  res.status(500).send({ success: false, error: "Error fetching logs: " + err.message });
116
- })
117
-
118
- })
118
+ });
119
+ });
119
120
 
120
121
 
121
122
  router.get('/flows/auth/:request_id', async (req, res) => {
@@ -0,0 +1,256 @@
1
+ const express = require('express');
2
+ const router = express.Router();
3
+ const { Namespace, UnansweredQuestion } = require('../models/kb_setting');
4
+ var winston = require('../config/winston');
5
+
6
+ // Add a new unanswered question
7
+ router.post('/', async (req, res) => {
8
+ try {
9
+ const { namespace, question } = req.body;
10
+ const id_project = req.projectid;
11
+
12
+ if (!namespace || !question) {
13
+ return res.status(400).json({
14
+ success: false,
15
+ error: "Missing required parameters: namespace and question"
16
+ });
17
+ }
18
+
19
+ // Check if namespace belongs to project
20
+ const isValidNamespace = await validateNamespace(id_project, namespace);
21
+ if (!isValidNamespace) {
22
+ return res.status(403).json({
23
+ success: false,
24
+ error: "Not allowed. The namespace does not belong to the current project."
25
+ });
26
+ }
27
+
28
+ const unansweredQuestion = new UnansweredQuestion({
29
+ id_project,
30
+ namespace,
31
+ question
32
+ });
33
+
34
+ const savedQuestion = await unansweredQuestion.save();
35
+ res.status(200).json(savedQuestion);
36
+
37
+ } catch (error) {
38
+ winston.error('Error adding unanswered question:', error);
39
+ res.status(500).json({
40
+ success: false,
41
+ error: "Error adding unanswered question"
42
+ });
43
+ }
44
+ });
45
+
46
+ // Get all unanswered questions for a namespace
47
+ router.get('/:namespace', async (req, res) => {
48
+ try {
49
+ const { namespace } = req.params;
50
+ const id_project = req.projectid;
51
+
52
+ if (!namespace) {
53
+ return res.status(400).json({
54
+ success: false,
55
+ error: "Missing required parameter: namespace"
56
+ });
57
+ }
58
+
59
+ // Check if namespace belongs to project
60
+ const isValidNamespace = await validateNamespace(id_project, namespace);
61
+ if (!isValidNamespace) {
62
+ return res.status(403).json({
63
+ success: false,
64
+ error: "Not allowed. The namespace does not belong to the current project."
65
+ });
66
+ }
67
+
68
+ const page = parseInt(req.query.page) || 0;
69
+ const limit = parseInt(req.query.limit) || 20;
70
+ const sortField = req.query.sortField || 'created_at';
71
+ const direction = parseInt(req.query.direction) || -1;
72
+
73
+ const questions = await UnansweredQuestion.find({
74
+ id_project,
75
+ namespace
76
+ })
77
+ .sort({ [sortField]: direction })
78
+ .skip(page * limit)
79
+ .limit(limit);
80
+
81
+ const count = await UnansweredQuestion.countDocuments({
82
+ id_project,
83
+ namespace
84
+ });
85
+
86
+ res.status(200).json({
87
+ count,
88
+ questions,
89
+ query: {
90
+ page,
91
+ limit,
92
+ sortField,
93
+ direction
94
+ }
95
+ });
96
+
97
+ } catch (error) {
98
+ winston.error('Error getting unanswered questions:', error);
99
+ res.status(500).json({
100
+ success: false,
101
+ error: "Error getting unanswered questions"
102
+ });
103
+ }
104
+ });
105
+
106
+ // Delete a specific unanswered question
107
+ router.delete('/:id', async (req, res) => {
108
+ try {
109
+ const { id } = req.params;
110
+ const id_project = req.projectid;
111
+
112
+ const question = await UnansweredQuestion.findOne({ _id: id, id_project });
113
+ if (!question) {
114
+ return res.status(404).json({
115
+ success: false,
116
+ error: "Question not found"
117
+ });
118
+ }
119
+
120
+ await UnansweredQuestion.deleteOne({ _id: id });
121
+ res.status(200).json({
122
+ success: true,
123
+ message: "Question deleted successfully"
124
+ });
125
+
126
+ } catch (error) {
127
+ winston.error('Error deleting unanswered question:', error);
128
+ res.status(500).json({
129
+ success: false,
130
+ error: "Error deleting unanswered question"
131
+ });
132
+ }
133
+ });
134
+
135
+ // Delete all unanswered questions for a namespace
136
+ router.delete('/namespace/:namespace', async (req, res) => {
137
+ try {
138
+ const { namespace } = req.params;
139
+ const id_project = req.projectid;
140
+
141
+ // Check if namespace belongs to project
142
+ const isValidNamespace = await validateNamespace(id_project, namespace);
143
+ if (!isValidNamespace) {
144
+ return res.status(403).json({
145
+ success: false,
146
+ error: "Not allowed. The namespace does not belong to the current project."
147
+ });
148
+ }
149
+
150
+ const result = await UnansweredQuestion.deleteMany({ id_project, namespace });
151
+ res.status(200).json({
152
+ success: true,
153
+ count: result.deletedCount,
154
+ message: "All questions deleted successfully"
155
+ });
156
+
157
+ } catch (error) {
158
+ winston.error('Error deleting unanswered questions:', error);
159
+ res.status(500).json({
160
+ success: false,
161
+ error: "Error deleting unanswered questions"
162
+ });
163
+ }
164
+ });
165
+
166
+ // Update an unanswered question
167
+ router.put('/:id', async (req, res) => {
168
+ try {
169
+ const { id } = req.params;
170
+ const { question } = req.body;
171
+ const id_project = req.projectid;
172
+
173
+ if (!question) {
174
+ return res.status(400).json({
175
+ success: false,
176
+ error: "Missing required parameter: question"
177
+ });
178
+ }
179
+
180
+ const updatedQuestion = await UnansweredQuestion.findOneAndUpdate(
181
+ { _id: id, id_project },
182
+ { question },
183
+ { new: true }
184
+ );
185
+
186
+ if (!updatedQuestion) {
187
+ return res.status(404).json({
188
+ success: false,
189
+ error: "Question not found"
190
+ });
191
+ }
192
+
193
+ res.status(200).json(updatedQuestion);
194
+
195
+ } catch (error) {
196
+ winston.error('Error updating unanswered question:', error);
197
+ res.status(500).json({
198
+ success: false,
199
+ error: "Error updating unanswered question"
200
+ });
201
+ }
202
+ });
203
+
204
+ // Count unanswered questions for a namespace
205
+ router.get('/count/:namespace', async (req, res) => {
206
+ try {
207
+ const { namespace } = req.params;
208
+ const id_project = req.projectid;
209
+
210
+ if (!namespace) {
211
+ return res.status(400).json({
212
+ success: false,
213
+ error: "Missing required parameter: namespace"
214
+ });
215
+ }
216
+
217
+ // Check if namespace belongs to project
218
+ const isValidNamespace = await validateNamespace(id_project, namespace);
219
+ if (!isValidNamespace) {
220
+ return res.status(403).json({
221
+ success: false,
222
+ error: "Not allowed. The namespace does not belong to the current project."
223
+ });
224
+ }
225
+
226
+ const count = await UnansweredQuestion.countDocuments({
227
+ id_project,
228
+ namespace
229
+ });
230
+
231
+ res.status(200).json({ count });
232
+
233
+ } catch (error) {
234
+ winston.error('Error counting unanswered questions:', error);
235
+ res.status(500).json({
236
+ success: false,
237
+ error: "Error counting unanswered questions"
238
+ });
239
+ }
240
+ });
241
+
242
+ // Helper function to validate namespace
243
+ async function validateNamespace(id_project, namespace_id) {
244
+ try {
245
+ const namespace = await Namespace.findOne({
246
+ id_project: id_project,
247
+ namespace: namespace_id
248
+ });
249
+ return !!namespace; // return true if namespace exists, false otherwise
250
+ } catch (err) {
251
+ winston.error("validate namespace error: ", err);
252
+ throw err;
253
+ }
254
+ }
255
+
256
+ module.exports = router;
@@ -7,14 +7,20 @@ const levels = { error: 0, warn: 1, info: 2, debug: 3, native: 4 };
7
7
 
8
8
  class LogsService {
9
9
 
10
- async getLastRows(request_id, limit, logLevel) {
10
+ async getLastRows(id, limit, logLevel, queryField = 'request_id') {
11
11
  let level = logLevel || default_log_level;
12
12
  if (level === 'default') {
13
13
  level = default_log_level
14
14
  }
15
15
  let nlevel = levels[level];
16
+
17
+ // Build match condition based on queryField
18
+ const matchCondition = queryField === 'webhook_id'
19
+ ? { [queryField]: id, longExp: { $exists: true } }
20
+ : { [queryField]: id };
21
+
16
22
  return FlowLogs.aggregate([
17
- { $match: { request_id: request_id } },
23
+ { $match: matchCondition },
18
24
  { $unwind: "$rows" },
19
25
  { $match: { "rows.nlevel": { $lte: nlevel } } },
20
26
  { $sort: { "rows.timestamp": -1, "rows._id": -1 } },
@@ -22,14 +28,20 @@ class LogsService {
22
28
  ]).then(rows => rows.reverse())
23
29
  }
24
30
 
25
- async getOlderRows(request_id, limit, logLevel, timestamp) {
31
+ async getOlderRows(id, limit, logLevel, timestamp, queryField = 'request_id') {
26
32
  let level = logLevel || default_log_level;
27
33
  if (level === 'default') {
28
34
  level = default_log_level
29
35
  }
30
36
  let nlevel = levels[level];
37
+
38
+ // Build match condition based on queryField
39
+ const matchCondition = queryField === 'webhook_id'
40
+ ? { [queryField]: id, longExp: { $exists: true } }
41
+ : { [queryField]: id };
42
+
31
43
  return FlowLogs.aggregate([
32
- { $match: { request_id: request_id } },
44
+ { $match: matchCondition },
33
45
  { $unwind: "$rows" },
34
46
  { $match: { "rows.nlevel": { $lte: nlevel }, "rows.timestamp": { $lt: timestamp } } },
35
47
  { $sort: { "rows.timestamp": -1, "rows._id": -1 } },
@@ -37,14 +49,20 @@ class LogsService {
37
49
  ]).then(rows => rows.reverse())
38
50
  }
39
51
 
40
- async getNewerRows(request_id, limit, logLevel, timestamp) {
52
+ async getNewerRows(id, limit, logLevel, timestamp, queryField = 'request_id') {
41
53
  let level = logLevel || default_log_level;
42
54
  if (level === 'default') {
43
55
  level = default_log_level
44
56
  }
45
57
  let nlevel = levels[level];
58
+
59
+ // Build match condition based on queryField
60
+ const matchCondition = queryField === 'webhook_id'
61
+ ? { [queryField]: id, longExp: { $exists: true } }
62
+ : { [queryField]: id };
63
+
46
64
  return FlowLogs.aggregate([
47
- { $match: { request_id: request_id } },
65
+ { $match: matchCondition },
48
66
  { $unwind: "$rows" },
49
67
  { $match: { "rows.nlevel": { $lte: nlevel }, "rows.timestamp": { $gt: timestamp } } },
50
68
  { $sort: { "rows.timestamp": 1, "rows._id": 1 } },
@@ -4,6 +4,7 @@ const uuidv4 = require('uuid/v4');
4
4
  var jwt = require('jsonwebtoken');
5
5
  var winston = require('../config/winston');
6
6
  const errorCodes = require("../errorCodes");
7
+ var ObjectId = require('mongoose').Types.ObjectId;
7
8
 
8
9
  const port = process.env.PORT || '3000';
9
10
  let TILEBOT_ENDPOINT = "http://localhost:" + port + "/modules/tilebot/";;
@@ -45,6 +46,8 @@ class WebhookService {
45
46
  let json_value = JSON.parse(value);
46
47
  payload.preloaded_request_id = json_value.request_id;
47
48
  payload.draft = true;
49
+ } else {
50
+ payload.preloaded_request_id = "automation-request-" + webhook.id_project + "-" + new ObjectId() + "-" + webhook.webhook_id;
48
51
  }
49
52
 
50
53
  let token = await this.generateChatbotToken(chatbot);