@tiledesk/tiledesk-server 2.28.2 → 2.29.0

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,139 @@
1
+ const Intent = require("../models/faq");
2
+ const AppError = require("../utils/AppError");
3
+ const faqBotEvent = require("../event/faqBotEvent");
4
+ const winston = require("../config/winston");
5
+
6
+ const INTENT_FIELDS = [
7
+ "intent_display_name",
8
+ "intent_id",
9
+ "question",
10
+ "answer",
11
+ "reply",
12
+ "form",
13
+ "enabled",
14
+ "webhook_enabled",
15
+ "language",
16
+ "actions",
17
+ "attributes"
18
+ ];
19
+
20
+ class IntentsController {
21
+
22
+ constructor() { }
23
+
24
+ _mapIntent(chatbot_id, id_project, user_id, intent) {
25
+ const mapped = {
26
+ id_faq_kb: chatbot_id,
27
+ id_project,
28
+ createdBy: user_id
29
+ };
30
+ for (const field of INTENT_FIELDS) {
31
+ if (intent[field] !== undefined) {
32
+ mapped[field] = intent[field];
33
+ }
34
+ }
35
+ return mapped;
36
+ }
37
+
38
+ async create(chatbot_id, id_project, user_id, intent, { skipDuplicates = false } = {}) {
39
+ try {
40
+ const faq = await Intent.create(
41
+ this._mapIntent(chatbot_id, id_project, user_id, intent)
42
+ );
43
+ faqBotEvent.emit("faq.create", faq);
44
+ return faq;
45
+ } catch (err) {
46
+ if (err.code === 11000) {
47
+ if (skipDuplicates) {
48
+ winston.debug("Skip duplicated intent_display_name");
49
+ return null;
50
+ }
51
+ throw new AppError(409, "Duplicate intent_display_name");
52
+ }
53
+ winston.error("(IntentsController) create error: ", err);
54
+ throw new AppError(500, "Error creating intent");
55
+ }
56
+ }
57
+
58
+ async createMany(chatbot_id, id_project, user_id, intents, options) {
59
+ if (!intents?.length) {
60
+ return [];
61
+ }
62
+ const results = await Promise.allSettled(
63
+ intents.map((intent) =>
64
+ this.create(chatbot_id, id_project, user_id, intent, options)
65
+ )
66
+ );
67
+ return results
68
+ .filter((r) => r.status === "fulfilled" && r.value)
69
+ .map((r) => r.value);
70
+ }
71
+
72
+ async upsert(chatbot_id, id_project, user_id, intent) {
73
+ try {
74
+ const savingResult = await Intent.findOneAndUpdate(
75
+ { id_faq_kb: chatbot_id, intent_display_name: intent.intent_display_name },
76
+ this._mapIntent(chatbot_id, id_project, user_id, intent),
77
+ { new: true, upsert: true, rawResult: true }
78
+ );
79
+
80
+ if (!savingResult?.value) {
81
+ return null;
82
+ }
83
+
84
+ if (savingResult.lastErrorObject?.updatedExisting === true) {
85
+ winston.verbose("updated existing intent");
86
+ faqBotEvent.emit("faq.update", savingResult.value);
87
+ } else {
88
+ winston.verbose("new intent created");
89
+ faqBotEvent.emit("faq.create", savingResult.value);
90
+ }
91
+
92
+ return savingResult.value;
93
+ } catch (err) {
94
+ winston.error("(IntentsController) upsert error: ", err);
95
+ throw new AppError(500, "Error upserting intent");
96
+ }
97
+ }
98
+
99
+ async upsertMany(chatbot_id, id_project, user_id, intents) {
100
+ if (!intents?.length) {
101
+ return [];
102
+ }
103
+ const results = await Promise.allSettled(
104
+ intents.map((intent) =>
105
+ this.upsert(chatbot_id, id_project, user_id, intent)
106
+ )
107
+ );
108
+ return results
109
+ .filter((r) => r.status === "fulfilled" && r.value)
110
+ .map((r) => r.value);
111
+ }
112
+
113
+ async deleteByChatbotId(chatbot_id) {
114
+ try {
115
+ const result = await Intent.deleteMany({ id_faq_kb: chatbot_id });
116
+ winston.verbose("All faq for chatbot " + chatbot_id + " deleted successfully");
117
+ return result;
118
+ } catch (err) {
119
+ winston.error("(IntentsController) deleteByChatbotId error: ", err);
120
+ throw new AppError(500, `Error deleting intents for chatbot ${chatbot_id}`);
121
+ }
122
+ }
123
+
124
+ async getIntents(chatbot_id) {
125
+ try {
126
+ const intents = await Intent
127
+ .find({ id_faq_kb: chatbot_id })
128
+ .lean()
129
+ .exec();
130
+ return intents;
131
+ } catch (err) {
132
+ winston.error("(IntentsController) getIntents error: ", err);
133
+ throw new AppError(500, `Error finding intents with chatbot id ${chatbot_id}`);
134
+ }
135
+ }
136
+ }
137
+
138
+ const intentsController = new IntentsController();
139
+ module.exports = intentsController;
package/errorCodes.js CHANGED
@@ -29,7 +29,8 @@ const errorCodes = {
29
29
  CHATBOT: {
30
30
  BASE_CODE: 12000,
31
31
  ERRORS: {
32
- DUPLICATE_SLUG: 12001
32
+ DUPLICATE_SLUG: 12001,
33
+ SUBAGENT_IN_USE: 12002
33
34
  }
34
35
  },
35
36
  WEBHOOK: {
package/event/botEvent.js CHANGED
@@ -135,7 +135,9 @@ class BotEvent extends EventEmitter {
135
135
  return;
136
136
  }
137
137
 
138
- await Faq.updateMany({ id_faq_kb: chatbot._id }, { trashed: true, trashedAt: chatbot.trashedAt }).catch((err) => {
138
+ const trashedAt = chatbot.trashedAt || new Date();
139
+
140
+ await Faq.updateMany({ id_faq_kb: chatbot._id }, { trashed: true, trashedAt: trashedAt }).catch((err) => {
139
141
  winston.error("Event faqbot.update.virtual.delete error updating faqs ", err);
140
142
  })
141
143
 
@@ -143,6 +145,9 @@ class BotEvent extends EventEmitter {
143
145
  winston.error("Error deleting webhook on chatbot deleting: ", err);
144
146
  })
145
147
 
148
+ // Soft-delete subagents of the draft chatbot (+ their intents) so TTL applies
149
+ await trashSubagentsByParentIds([chatbot._id], trashedAt);
150
+
146
151
  let publishedChatbots = await Faq_kb.find({ root_id: chatbot._id }, { _id: 1 }).catch((err) => {
147
152
  winston.error("Event faqbot.update.virtual.delete error getting all published chatbots ", err);
148
153
  })
@@ -161,13 +166,16 @@ class BotEvent extends EventEmitter {
161
166
  for (const batch of batches) {
162
167
  await Faq_kb.updateMany(
163
168
  { _id: { $in: batch } },
164
- { $set: { trashed: true, trashedAt: chatbot.trashedAt } }
169
+ { $set: { trashed: true, trashedAt: trashedAt } }
165
170
  );
166
171
 
167
172
  await Faq.updateMany(
168
173
  { id_faq_kb: { $in: batch } },
169
- { $set: { trashed: true, trashedAt: chatbot.trashedAt } }
174
+ { $set: { trashed: true, trashedAt: trashedAt } }
170
175
  );
176
+
177
+ // Soft-delete subagents linked to published copies (+ their intents)
178
+ await trashSubagentsByParentIds(batch, trashedAt);
171
179
 
172
180
  await sleep(sleep_ms);
173
181
  }
@@ -175,6 +183,38 @@ class BotEvent extends EventEmitter {
175
183
  }
176
184
  })
177
185
 
186
+ async function trashSubagentsByParentIds(parentIds, trashedAt) {
187
+ if (!parentIds || parentIds.length === 0) {
188
+ return;
189
+ }
190
+
191
+ const parentIdStrings = parentIds.map(id => id.toString());
192
+ let subagents = await Faq_kb.find({ parent_id: { $in: parentIdStrings } }, { _id: 1 }).catch((err) => {
193
+ winston.error("Event faqbot.update.virtual.delete error getting subagents ", err);
194
+ return [];
195
+ });
196
+
197
+ if (!subagents || subagents.length === 0) {
198
+ return;
199
+ }
200
+
201
+ const subagentIds = subagents.map(s => s._id);
202
+
203
+ await Faq_kb.updateMany(
204
+ { _id: { $in: subagentIds } },
205
+ { $set: { trashed: true, trashedAt: trashedAt } }
206
+ ).catch((err) => {
207
+ winston.error("Event faqbot.update.virtual.delete error updating subagents ", err);
208
+ });
209
+
210
+ await Faq.updateMany(
211
+ { id_faq_kb: { $in: subagentIds } },
212
+ { $set: { trashed: true, trashedAt: trashedAt } }
213
+ ).catch((err) => {
214
+ winston.error("Event faqbot.update.virtual.delete error updating subagent faqs ", err);
215
+ });
216
+ }
217
+
178
218
  function sleep(ms) {
179
219
  return new Promise(resolve => setTimeout(resolve, ms));
180
220
  }
@@ -0,0 +1,41 @@
1
+ const AppError = require('../utils/AppError');
2
+ const winston = require('../config/winston');
3
+
4
+ function errorHandler(err, req, res, next) {
5
+ if (err instanceof AppError) {
6
+ winston.error(err.message, err);
7
+ return res.status(err.status).json({
8
+ success: false,
9
+ message: err.message
10
+ });
11
+ }
12
+
13
+ winston.debug("err.name", err.name);
14
+ if (err.name === "IpDeniedError") {
15
+ winston.debug("IpDeniedError");
16
+ return res.status(401).json({ err: "error ip filter" });
17
+ }
18
+
19
+ const realIp = req.headers['x-forwarded-for']?.split(',')[0] || req.headers['x-real-ip'] || req.ip;
20
+
21
+ // emitted by multer when the file is too big
22
+ if (err.code === "LIMIT_FILE_SIZE") {
23
+ winston.debug("LIMIT_FILE_SIZE");
24
+ winston.warn(`LIMIT_FILE_SIZE on ${req.originalUrl}`, {
25
+ limit: process.env.MAX_UPLOAD_FILE_SIZE,
26
+ ip: req.ip,
27
+ realIp: realIp
28
+ });
29
+ return res.status(413).json({ err: "Content Too Large", limit_file_size: process.env.MAX_UPLOAD_FILE_SIZE });
30
+ }
31
+
32
+ if (err.type === "entity.too.large" || err.name === "PayloadTooLargeError") {
33
+ winston.warn("Payload too large", { expected: err.expected, limit: err.limit, length: err.length });
34
+ return res.status(413).json({ err: "Request entity too large", limit: err.limit });
35
+ }
36
+
37
+ winston.error("General error: ", err);
38
+ return res.status(500).json({ err: "error" });
39
+ }
40
+
41
+ module.exports = errorHandler;
@@ -18,5 +18,9 @@ module.exports = {
18
18
  voice_twilio: {
19
19
  default: 'blank_voice_twilio',
20
20
  templates: ['empty', 'blank_voice_twilio']
21
+ },
22
+ subagent: {
23
+ default: 'blank_subagent',
24
+ templates: ['empty', 'blank_subagent']
21
25
  }
22
26
  }
package/models/faq.js CHANGED
@@ -192,6 +192,16 @@ FaqSchema.index(
192
192
  { partialFilterExpression: { "actions.namespace": { $exists: true } } }
193
193
  );
194
194
 
195
+ FaqSchema.index(
196
+ { "actions.botId": 1 },
197
+ { partialFilterExpression: { "actions.botId": { $exists: true } } }
198
+ );
199
+
200
+ FaqSchema.index(
201
+ { "actions.botSlug": 1 },
202
+ { partialFilterExpression: { "actions.botSlug": { $exists: true } } }
203
+ );
204
+
195
205
  FaqSchema.index(
196
206
  { trashedAt: 1 },
197
207
  { expireAfterSeconds: trashExpirationTime }
package/models/faq_kb.js CHANGED
@@ -161,6 +161,10 @@ var Faq_kbSchema = new Schema({
161
161
  release_note: {
162
162
  type: String,
163
163
  required: false
164
+ },
165
+ parent_id: {
166
+ type: String,
167
+ required: false
164
168
  }
165
169
  },{
166
170
  timestamps: true
@@ -248,6 +252,16 @@ Faq_kbSchema.index(
248
252
  { unique: true, partialFilterExpression: { slug: { $exists: true } } }
249
253
  );
250
254
 
255
+ Faq_kbSchema.index(
256
+ { id_project: 1, subtype: 1 },
257
+ { partialFilterExpression: { subtype: { $ne: "subagent" } } }
258
+ );
259
+
260
+ Faq_kbSchema.index(
261
+ { id_project: 1, parent_id: 1 },
262
+ { partialFilterExpression: { parent_id: { $exists: true } } }
263
+ );
264
+
251
265
  Faq_kbSchema.index(
252
266
  { trashedAt: 1 },
253
267
  { expireAfterSeconds: trashExpirationTime }
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.28.2",
4
+ "version": "2.29.0",
5
5
  "scripts": {
6
6
  "start": "node ./bin/www",
7
7
  "pretest": "mongodb-runner start",
@@ -49,7 +49,7 @@
49
49
  "@tiledesk/tiledesk-rasa-connector": "^1.0.10",
50
50
  "@tiledesk/tiledesk-sms-connector": "^0.1.13",
51
51
  "@tiledesk/tiledesk-telegram-connector": "^0.1.14",
52
- "@tiledesk/tiledesk-tybot-connector": "^2.1.15",
52
+ "@tiledesk/tiledesk-tybot-connector": "^2.3.0",
53
53
  "@tiledesk/tiledesk-voice-twilio-connector": "^0.3.2",
54
54
  "@tiledesk/tiledesk-vxml-connector": "^0.1.91",
55
55
  "@tiledesk/tiledesk-whatsapp-connector": "1.0.31",