@tiledesk/tiledesk-server 2.18.5 → 2.18.16
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 +24 -0
- package/app.js +2 -1
- package/event/kbEvent.js +12 -0
- package/event/webhookEvent.js +9 -0
- package/jobs.js +12 -1
- package/lib/analyticsClient.js +60 -0
- package/package.json +2 -2
- package/pubmodules/analytics-publisher/index.js +437 -0
- package/pubmodules/pubModulesManager.js +14 -0
- package/routes/answered.js +73 -0
- package/routes/kb.js +20 -1
- package/routes/project_user.js +51 -20
- package/routes/public-request.js +305 -239
- package/routes/request.js +11 -1
- package/routes/unanswered.js +71 -0
- package/routes/urlPreview.js +57 -0
- package/routes/webhook.js +2 -0
- package/services/Scheduler.js +13 -0
- package/services/aiManager.js +30 -0
- package/services/urlPreviewService.js +50 -0
- package/utils/jobs-worker-queue-manager/JobManagerV2.js +23 -12
- package/utils/jobs-worker-queue-manager/queueManagerClassV2.js +270 -270
- package/utils/transcriptTimezone.js +101 -0
- package/views/messages-layout.jade +130 -0
- package/views/messages.jade +23 -22
- package/views/messages_old.jade +11 -4
- package/.env.sample +0 -141
package/routes/unanswered.js
CHANGED
|
@@ -2,6 +2,7 @@ const express = require('express');
|
|
|
2
2
|
const router = express.Router();
|
|
3
3
|
const { Namespace, UnansweredQuestion } = require('../models/kb_setting');
|
|
4
4
|
var winston = require('../config/winston');
|
|
5
|
+
var fastCsv = require('fast-csv');
|
|
5
6
|
|
|
6
7
|
// Add a new unanswered question
|
|
7
8
|
router.post('/', async (req, res) => {
|
|
@@ -252,6 +253,76 @@ router.get('/count/:namespace', async (req, res) => {
|
|
|
252
253
|
}
|
|
253
254
|
});
|
|
254
255
|
|
|
256
|
+
router.get('/:namespace/export', async (req, res) => {
|
|
257
|
+
try {
|
|
258
|
+
const { namespace } = req.params;
|
|
259
|
+
const id_project = req.projectid;
|
|
260
|
+
const mode = String(req.query.mode || 'csv').toLowerCase();
|
|
261
|
+
|
|
262
|
+
if (mode !== 'csv' && mode !== 'json') {
|
|
263
|
+
return res.status(400).json({
|
|
264
|
+
success: false,
|
|
265
|
+
error: 'Invalid format. Use mode=json or mode=csv'
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const isValidNamespace = await validateNamespace(id_project, namespace);
|
|
270
|
+
if (!isValidNamespace) {
|
|
271
|
+
return res.status(403).json({
|
|
272
|
+
success: false,
|
|
273
|
+
error: "Not allowed. The namespace does not belong to the current project."
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const questions = await UnansweredQuestion.find({ id_project, namespace })
|
|
278
|
+
.sort({ createdAt: -1 })
|
|
279
|
+
.lean();
|
|
280
|
+
|
|
281
|
+
const safeFilename = String(namespace).replace(/[^\w.-]+/g, '_') || 'export';
|
|
282
|
+
|
|
283
|
+
if (mode === 'json') {
|
|
284
|
+
const questionsJson = questions.map((q) => {
|
|
285
|
+
const { __v, updatedAt, ...doc } = q;
|
|
286
|
+
return doc;
|
|
287
|
+
});
|
|
288
|
+
const payload = {
|
|
289
|
+
namespace,
|
|
290
|
+
exportedAt: new Date().toISOString(),
|
|
291
|
+
count: questionsJson.length,
|
|
292
|
+
questions: questionsJson
|
|
293
|
+
};
|
|
294
|
+
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
295
|
+
res.setHeader('Content-Disposition', `attachment; filename="unanswered-${safeFilename}.json"`);
|
|
296
|
+
return res.send(JSON.stringify(payload, null, 2));
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
|
|
300
|
+
res.setHeader('Content-Disposition', `attachment; filename="unanswered-${safeFilename}.csv"`);
|
|
301
|
+
|
|
302
|
+
const csvStream = fastCsv.format({ headers: true });
|
|
303
|
+
csvStream.pipe(res);
|
|
304
|
+
|
|
305
|
+
for (const q of questions) {
|
|
306
|
+
csvStream.write({
|
|
307
|
+
id: String(q._id),
|
|
308
|
+
namespace: q.namespace,
|
|
309
|
+
question: q.question,
|
|
310
|
+
request_id: q.request_id || '',
|
|
311
|
+
createdAt: q.createdAt ? new Date(q.createdAt).toISOString() : ''
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
csvStream.end();
|
|
315
|
+
} catch (error) {
|
|
316
|
+
winston.error('Error exporting unanswered questions:', error);
|
|
317
|
+
if (!res.headersSent) {
|
|
318
|
+
res.status(500).json({
|
|
319
|
+
success: false,
|
|
320
|
+
error: "Error exporting unanswered questions"
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
|
|
255
326
|
// Helper function to validate namespace
|
|
256
327
|
async function validateNamespace(id_project, namespace_id) {
|
|
257
328
|
try {
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const router = express.Router();
|
|
3
|
+
const winston = require('../config/winston');
|
|
4
|
+
const urlPreviewService = require('../services/urlPreviewService');
|
|
5
|
+
|
|
6
|
+
const PRIVATE_IP = /^(127\.|10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.|0\.0\.0\.0|::1)/;
|
|
7
|
+
const MAX_URLS = 10;
|
|
8
|
+
|
|
9
|
+
function isSafeUrl(rawUrl) {
|
|
10
|
+
try {
|
|
11
|
+
const { protocol, hostname } = new URL(rawUrl);
|
|
12
|
+
if (!['http:', 'https:'].includes(protocol)) return false;
|
|
13
|
+
if (PRIVATE_IP.test(hostname)) return false;
|
|
14
|
+
return true;
|
|
15
|
+
} catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// POST /:projectid/url-preview — public endpoint (used by the widget without auth)
|
|
21
|
+
// Body: { "urls": ["https://example.com", "https://another.com"] }
|
|
22
|
+
router.post('/', async (req, res) => {
|
|
23
|
+
const urls = req.body.urls;
|
|
24
|
+
|
|
25
|
+
if (!Array.isArray(urls) || urls.length === 0) {
|
|
26
|
+
return res.status(422).send({ success: false, error: 'urls must be a non-empty array' });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (urls.length > MAX_URLS) {
|
|
30
|
+
return res.status(422).send({ success: false, error: `urls must contain at most ${MAX_URLS} items` });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const safeUrls = [];
|
|
34
|
+
const blocked = [];
|
|
35
|
+
for (const url of urls) {
|
|
36
|
+
if (isSafeUrl(url)) {
|
|
37
|
+
safeUrls.push(url);
|
|
38
|
+
} else {
|
|
39
|
+
blocked.push({ url, success: false, error: 'URL not allowed' });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const fetched = safeUrls.length > 0 ? await urlPreviewService.fetchPagesPreviews(safeUrls) : [];
|
|
45
|
+
// Merge results preserving original order
|
|
46
|
+
const resultMap = new Map(fetched.map(r => [r.url, r]));
|
|
47
|
+
const previews = urls.map(url =>
|
|
48
|
+
resultMap.get(url) || blocked.find(b => b.url === url)
|
|
49
|
+
);
|
|
50
|
+
res.status(200).send(previews);
|
|
51
|
+
} catch (err) {
|
|
52
|
+
winston.error('[urlPreview] error', err);
|
|
53
|
+
res.status(500).send({ success: false, error: err.message || 'Internal error' });
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
module.exports = router;
|
package/routes/webhook.js
CHANGED
|
@@ -6,6 +6,7 @@ const JobManager = require('../utils/jobs-worker-queue-manager/JobManagerV2');
|
|
|
6
6
|
const { AiReindexService } = require('../services/aiReindexService');
|
|
7
7
|
const { Webhook } = require('../models/webhook');
|
|
8
8
|
const webhookService = require('../services/webhookService');
|
|
9
|
+
const webhookEvent = require('../event/webhookEvent');
|
|
9
10
|
const errorCodes = require('../errorCodes');
|
|
10
11
|
const aiManager = require('../services/aiManager');
|
|
11
12
|
var ObjectId = require('mongoose').Types.ObjectId;
|
|
@@ -311,6 +312,7 @@ router.all('/:webhook_id', async (req, res) => {
|
|
|
311
312
|
//webhookService.run(webhook, payload)
|
|
312
313
|
// To delete - End
|
|
313
314
|
webhookService.run(webhook, payload, dev, redis_client).then((response) => {
|
|
315
|
+
webhookEvent.emit("webhook.triggered", { webhook: webhook, payload: payload });
|
|
314
316
|
return res.status(200).send(response);
|
|
315
317
|
}).catch((err) => {
|
|
316
318
|
if (err.code === errorCodes.WEBHOOK.ERRORS.NO_PRELOADED_DEV_REQUEST) {
|
package/services/Scheduler.js
CHANGED
|
@@ -46,6 +46,19 @@ class Scheduler {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
}
|
|
49
|
+
|
|
50
|
+
deleteSchedule(data, callback) {
|
|
51
|
+
winston.debug("(deleteScheduler) data: ", data);
|
|
52
|
+
this.jobManager.publishDelete(data, (err, ok) => {
|
|
53
|
+
let response_data = { success: true, message: "Scheduled" };
|
|
54
|
+
if (err) {
|
|
55
|
+
response_data = { success: false, message: "Task not scheduled" };
|
|
56
|
+
}
|
|
57
|
+
if (callback) {
|
|
58
|
+
callback(err, response_data);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
49
62
|
|
|
50
63
|
}
|
|
51
64
|
|
package/services/aiManager.js
CHANGED
|
@@ -412,6 +412,36 @@ class AiManager {
|
|
|
412
412
|
})
|
|
413
413
|
}
|
|
414
414
|
|
|
415
|
+
async deleteSitemap(sitemapContent, namespace) {
|
|
416
|
+
|
|
417
|
+
const sitemapContentId = sitemapContent._id;
|
|
418
|
+
const relatedContents = await KB.find({ id_project: sitemapContent.id_project, namespace: sitemapContent.namespace, sitemap_origin_id: sitemapContentId }).lean().exec();
|
|
419
|
+
|
|
420
|
+
if (!relatedContents.length) {
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const engine = namespace.engine || default_engine;
|
|
425
|
+
const scheduler = new Scheduler({ jobManager: jobManager });
|
|
426
|
+
|
|
427
|
+
await Promise.all(relatedContents.map((kb) => {
|
|
428
|
+
return new Promise((resolve) => {
|
|
429
|
+
const data = {
|
|
430
|
+
id: kb._id,
|
|
431
|
+
namespace: kb.namespace,
|
|
432
|
+
engine: engine
|
|
433
|
+
};
|
|
434
|
+
scheduler.deleteSchedule(data, (err, result) => {
|
|
435
|
+
if (err) {
|
|
436
|
+
winston.error("deleteSitemap: deleteSchedule error for kb " + kb._id, err);
|
|
437
|
+
}
|
|
438
|
+
console.log("deleteSitemap: deleteSchedule result for kb " + kb._id, result);
|
|
439
|
+
resolve(result);
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
}));
|
|
443
|
+
}
|
|
444
|
+
|
|
415
445
|
async saveBulk(operations, kbs, project_id, namespace) {
|
|
416
446
|
|
|
417
447
|
return new Promise((resolve, reject) => {
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const axios = require('axios');
|
|
2
|
+
const winston = require('../config/winston');
|
|
3
|
+
|
|
4
|
+
class UrlPreviewService {
|
|
5
|
+
|
|
6
|
+
_extractMeta(html, url) {
|
|
7
|
+
const getMeta = (name) => {
|
|
8
|
+
const m = html.match(new RegExp(`<meta[^>]+(?:name|property)=["']${name}["'][^>]+content=["']([^"']+)["']`, 'i'))
|
|
9
|
+
|| html.match(new RegExp(`<meta[^>]+content=["']([^"']+)["'][^>]+(?:name|property)=["']${name}["']`, 'i'));
|
|
10
|
+
return m ? m[1] : null;
|
|
11
|
+
};
|
|
12
|
+
const titleMatch = html.match(/<title[^>]*>([^<]+)<\/title>/i);
|
|
13
|
+
return {
|
|
14
|
+
url,
|
|
15
|
+
title: getMeta('og:title') || (titleMatch && titleMatch[1].trim()) || null,
|
|
16
|
+
description: getMeta('og:description') || getMeta('description') || null,
|
|
17
|
+
image: getMeta('og:image') || getMeta('twitter:image') || null,
|
|
18
|
+
siteName: getMeta('og:site_name') || null,
|
|
19
|
+
type: getMeta('og:type') || null,
|
|
20
|
+
locale: getMeta('og:locale') || null,
|
|
21
|
+
author: getMeta('author') || getMeta('article:author') || null,
|
|
22
|
+
keywords: getMeta('keywords') || null,
|
|
23
|
+
twitterCard: getMeta('twitter:card') || null,
|
|
24
|
+
twitterTitle: getMeta('twitter:title') || null,
|
|
25
|
+
twitterDescription: getMeta('twitter:description') || null,
|
|
26
|
+
twitterImage: getMeta('twitter:image') || null,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async fetchPagePreview(url) {
|
|
31
|
+
const response = await axios.get(url, {
|
|
32
|
+
timeout: 10000,
|
|
33
|
+
headers: { 'User-Agent': 'Mozilla/5.0 (compatible; TiledeskBot/1.0)' },
|
|
34
|
+
maxContentLength: 2000000,
|
|
35
|
+
});
|
|
36
|
+
return this._extractMeta(response.data, url);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async fetchPagesPreviews(urls) {
|
|
40
|
+
const results = await Promise.allSettled(urls.map(url => this.fetchPagePreview(url)));
|
|
41
|
+
return results.map((result, i) => {
|
|
42
|
+
if (result.status === 'fulfilled') {
|
|
43
|
+
return { url: urls[i], success: true, data: result.value };
|
|
44
|
+
}
|
|
45
|
+
return { url: urls[i], success: false, error: result.reason?.message || 'Failed to fetch' };
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = new UrlPreviewService();
|
|
@@ -21,15 +21,7 @@ class JobManager {
|
|
|
21
21
|
var that = this;
|
|
22
22
|
if (this.info) {console.log("[JobWorker] JobManager publisher started");}
|
|
23
23
|
|
|
24
|
-
this.queueManager.connect(function(
|
|
25
|
-
|
|
26
|
-
if (err) {
|
|
27
|
-
console.log("[JobWorker] - connectAndStartPublisher - connection error: ", err);
|
|
28
|
-
if (callback) {
|
|
29
|
-
callback(null, err)
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
24
|
+
this.queueManager.connect(function() {
|
|
33
25
|
if (that.debug) {console.log("[JobWorker] Queue started");}
|
|
34
26
|
that.queuePublisherConnected = true;
|
|
35
27
|
|
|
@@ -49,8 +41,7 @@ class JobManager {
|
|
|
49
41
|
}
|
|
50
42
|
|
|
51
43
|
if (callback) {
|
|
52
|
-
callback(
|
|
53
|
-
return;
|
|
44
|
+
callback();
|
|
54
45
|
}
|
|
55
46
|
});
|
|
56
47
|
});
|
|
@@ -108,10 +99,30 @@ class JobManager {
|
|
|
108
99
|
// this.connectAndStartPublisher();
|
|
109
100
|
// this.sendingJobs.push(packet);
|
|
110
101
|
|
|
111
|
-
//
|
|
102
|
+
// }
|
|
112
103
|
|
|
113
104
|
}
|
|
114
105
|
|
|
106
|
+
publishDelete(payload, callback) {
|
|
107
|
+
|
|
108
|
+
var packet = { payload: payload };
|
|
109
|
+
const routingKey = this.queueManager.deleteRoutingKey;
|
|
110
|
+
|
|
111
|
+
if (this.queuePublisherConnected == true) {
|
|
112
|
+
|
|
113
|
+
if (this.debug) { console.log("[JobWorker] JobManager publishDelete routingKey: " + routingKey); }
|
|
114
|
+
this.queueManager.sendJson(packet, routingKey, (err, ok) => {
|
|
115
|
+
if (err) {
|
|
116
|
+
console.error("sendJson (delete) error: ", err);
|
|
117
|
+
} else {
|
|
118
|
+
if (this.debug) { console.log("sendJson (delete) ok"); }
|
|
119
|
+
}
|
|
120
|
+
callback(err, ok);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
115
126
|
|
|
116
127
|
//Deprecated
|
|
117
128
|
schedule(fn, payload) {
|