@yrpri/api 9.0.89 → 9.0.91
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/active-citizen/llms/collectionImageGenerator.js +44 -5
- package/agents/assistants/baseAssistant.js +1 -1
- package/agents/assistants/modes/tools/models/subscriptions.js +5 -1
- package/agents/assistants/modes/tools/navigationTools.js +27 -10
- package/agents/assistants/voiceAssistant.js +1 -1
- package/agents/models/testData/updateAgentWorkflowConfiguration.js +1 -1
- package/controllers/images.cjs +64 -10
- package/controllers/nonSpa.cjs +1 -1
- package/package.json +2 -2
- package/scripts/cleanups/deleteAnonNotifications.cjs +91 -0
- package/scripts/cleanups/deleteYearOldNotifications.cjs +72 -0
- package/utils/sitemap_generator.cjs +12 -0
|
@@ -16,11 +16,50 @@ const disableFlux = false;
|
|
|
16
16
|
export class CollectionImageGenerator {
|
|
17
17
|
async resizeImage(imagePath, width, height) {
|
|
18
18
|
const resizedImageFilePath = path.join("/tmp", `${uuidv4()}.png`);
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
.
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
try {
|
|
20
|
+
// 1) Initialize Sharp instance
|
|
21
|
+
const image = sharp(imagePath).rotate(); // rotate fixes orientation from EXIF
|
|
22
|
+
// 2) Read metadata to validate format
|
|
23
|
+
const metadata = await image.metadata();
|
|
24
|
+
const validFormats = [
|
|
25
|
+
"jpeg",
|
|
26
|
+
"png",
|
|
27
|
+
"webp",
|
|
28
|
+
"gif",
|
|
29
|
+
"tiff",
|
|
30
|
+
"avif",
|
|
31
|
+
"svg"
|
|
32
|
+
];
|
|
33
|
+
if (!metadata.format || !validFormats.includes(metadata.format)) {
|
|
34
|
+
throw new Error(`Unsupported format: ${metadata.format} (expected one of ${validFormats.join(", ")})`);
|
|
35
|
+
}
|
|
36
|
+
// 3) Resize + convert
|
|
37
|
+
await image
|
|
38
|
+
.resize({
|
|
39
|
+
width,
|
|
40
|
+
height,
|
|
41
|
+
fit: "inside",
|
|
42
|
+
withoutEnlargement: true, // ensures you won't upscale smaller images
|
|
43
|
+
})
|
|
44
|
+
.toFormat("png", {
|
|
45
|
+
quality: 90,
|
|
46
|
+
progressive: true,
|
|
47
|
+
})
|
|
48
|
+
.toFile(resizedImageFilePath);
|
|
49
|
+
// 4) Remove the original file after successful resize
|
|
50
|
+
fs.unlinkSync(imagePath);
|
|
51
|
+
return resizedImageFilePath;
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
// Cleanup if something goes wrong
|
|
55
|
+
console.error("Error resizing image:", err);
|
|
56
|
+
// Optionally remove partial or empty output file if it exists
|
|
57
|
+
if (fs.existsSync(resizedImageFilePath)) {
|
|
58
|
+
fs.unlinkSync(resizedImageFilePath);
|
|
59
|
+
}
|
|
60
|
+
// Rethrow or handle error further
|
|
61
|
+
throw err;
|
|
62
|
+
}
|
|
24
63
|
}
|
|
25
64
|
async downloadImage(imageUrl, imageFilePath) {
|
|
26
65
|
const response = await axios({
|
|
@@ -21,7 +21,7 @@ export class YpBaseAssistant extends YpBaseChatBot {
|
|
|
21
21
|
this.availableTools = new Map();
|
|
22
22
|
this.toolCallTimeout = 30000; // 30 seconds
|
|
23
23
|
this.maxModeTransitions = 10;
|
|
24
|
-
this.modelName = "gpt-4o";
|
|
24
|
+
this.modelName = process.env.OPENAI_STREAMING_MODEL_NAME || "gpt-4o-2024-11-20";
|
|
25
25
|
this.defaultSystemPrompt = `<coreImportantSystemInstructions>
|
|
26
26
|
You are a helpful, witty, and friendly AI. Act like a human, but remember that you aren't a human and that you can't do human things in the real world.
|
|
27
27
|
Your voice and personality should be warm and engaging, with a lively and playful tone. Talk quickly.
|
|
@@ -3,6 +3,7 @@ import { YpSubscriptionPlan } from "../../../../models/subscriptionPlan.js";
|
|
|
3
3
|
import { YpAgentProduct } from "../../../../models/agentProduct.js";
|
|
4
4
|
import { YpAgentProductBundle } from "../../../../models/agentProductBundle.js";
|
|
5
5
|
import { YpAgentProductRun } from "../../../../models/agentProductRun.js";
|
|
6
|
+
import { Op } from "sequelize";
|
|
6
7
|
export class SubscriptionModels {
|
|
7
8
|
constructor(assistant) {
|
|
8
9
|
this.assistant = assistant;
|
|
@@ -18,7 +19,10 @@ export class SubscriptionModels {
|
|
|
18
19
|
// Get all available subscription plans with their associated agent products
|
|
19
20
|
const availablePlans = await YpSubscriptionPlan.findAll({
|
|
20
21
|
where: {
|
|
21
|
-
|
|
22
|
+
id: {
|
|
23
|
+
[Op.in]: [1, 6],
|
|
24
|
+
},
|
|
25
|
+
// status: 'active', // Only get active plans
|
|
22
26
|
},
|
|
23
27
|
attributes: ['id', 'configuration', 'name', 'description'],
|
|
24
28
|
include: [
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// commonTools.ts
|
|
2
|
-
import { AgentModels } from
|
|
3
|
-
import { SubscriptionModels } from
|
|
4
|
-
import { BaseAssistantTools } from
|
|
2
|
+
import { AgentModels } from "./models/agents.js";
|
|
3
|
+
import { SubscriptionModels } from "./models/subscriptions.js";
|
|
4
|
+
import { BaseAssistantTools } from "./baseTools.js";
|
|
5
5
|
export class NavigationTools extends BaseAssistantTools {
|
|
6
6
|
constructor(assistant) {
|
|
7
7
|
super(assistant);
|
|
@@ -9,10 +9,10 @@ export class NavigationTools extends BaseAssistantTools {
|
|
|
9
9
|
this.subscriptionModels = new SubscriptionModels(assistant);
|
|
10
10
|
}
|
|
11
11
|
async goBackToMainAssistant() {
|
|
12
|
-
await this.assistant.handleModeSwitch(
|
|
12
|
+
await this.assistant.handleModeSwitch("agent_selection_mode", "User requested to return to the main assistant", {});
|
|
13
13
|
return {
|
|
14
14
|
success: true,
|
|
15
|
-
data: { message:
|
|
15
|
+
data: { message: "Returned to main assistant" },
|
|
16
16
|
};
|
|
17
17
|
}
|
|
18
18
|
get connectDirectlyToAgent() {
|
|
@@ -36,12 +36,31 @@ export class NavigationTools extends BaseAssistantTools {
|
|
|
36
36
|
params = this.assistant.getCleanedParams(params);
|
|
37
37
|
console.log(`handler: connect_to_one_of_the_agents: ${JSON.stringify(params, null, 2)}`);
|
|
38
38
|
try {
|
|
39
|
-
|
|
39
|
+
let { plan, subscription } = await this.subscriptionModels.loadAgentProductPlanAndSubscription(params.subscriptionPlanId);
|
|
40
40
|
if (!plan?.AgentProduct) {
|
|
41
41
|
throw new Error(`Agent product with id ${params.subscriptionPlanId} not found`);
|
|
42
42
|
}
|
|
43
|
+
//TODO: Does not work as the user is not logged in. Lets figure it out.
|
|
44
|
+
/*if (plan.configuration.amount == 0) {
|
|
45
|
+
const result = await this.subscriptionModels.subscribeToAgentPlan(
|
|
46
|
+
plan.AgentProduct!.id,
|
|
47
|
+
plan.id
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
if (!result.success || !result.subscription || !result.plan) {
|
|
51
|
+
return {
|
|
52
|
+
success: false,
|
|
53
|
+
error: result.error || "Failed to subscribe to agent plan",
|
|
54
|
+
};
|
|
55
|
+
} else {
|
|
56
|
+
plan = result.plan;
|
|
57
|
+
subscription = result.subscription;
|
|
58
|
+
}
|
|
59
|
+
}*/
|
|
43
60
|
console.log(`Loading: ${plan?.AgentProduct?.name} ${subscription?.id}`);
|
|
44
|
-
await this.updateCurrentAgentProductPlan(plan, subscription, {
|
|
61
|
+
await this.updateCurrentAgentProductPlan(plan, subscription, {
|
|
62
|
+
sendEvent: false,
|
|
63
|
+
});
|
|
45
64
|
await this.assistant.handleModeSwitch("agent_direct_connection_mode", `Directly connected to agent: ${plan?.AgentProduct?.name}`, params);
|
|
46
65
|
const html = `<div class="agent-chips"><yp-agent-chip
|
|
47
66
|
isSelected
|
|
@@ -61,9 +80,7 @@ export class NavigationTools extends BaseAssistantTools {
|
|
|
61
80
|
};
|
|
62
81
|
}
|
|
63
82
|
catch (error) {
|
|
64
|
-
const errorMessage = error instanceof Error
|
|
65
|
-
? error.message
|
|
66
|
-
: "Failed to select agent";
|
|
83
|
+
const errorMessage = error instanceof Error ? error.message : "Failed to select agent";
|
|
67
84
|
console.error(`Failed to select agent: ${errorMessage}`);
|
|
68
85
|
return {
|
|
69
86
|
success: false,
|
|
@@ -19,7 +19,7 @@ export class YpBaseChatBotWithVoice extends YpBaseChatBot {
|
|
|
19
19
|
};
|
|
20
20
|
// Default voice configuration
|
|
21
21
|
this.voiceConfig = {
|
|
22
|
-
model: "gpt-4o-realtime-preview-2024-12-17",
|
|
22
|
+
model: process.env.OPENAI_VOICE_MODEL_NAME || "gpt-4o-realtime-preview-2024-12-17",
|
|
23
23
|
voice: "echo",
|
|
24
24
|
modalities: ["text", "audio"],
|
|
25
25
|
};
|
|
@@ -50,7 +50,7 @@ async function setupAgentProductsConfiguration() {
|
|
|
50
50
|
},
|
|
51
51
|
{
|
|
52
52
|
name: "Competitor analysis report",
|
|
53
|
-
shortName: "Competitor
|
|
53
|
+
shortName: "Competitor Report",
|
|
54
54
|
shortDescription: "Report on the state of your key competitors and recommendations.",
|
|
55
55
|
description: "Report on the state of your key competitors where all the information gathered in earlier steps has been consolidated into chapters for each competitor and concludes with a chapter on action-oriented recommendations.",
|
|
56
56
|
agentClassUuid: "1cf3af64-a5f6-a7c1-91c1-51fb13c72f1a",
|
package/controllers/images.cjs
CHANGED
|
@@ -222,26 +222,71 @@ router.post("/:imageId/comment", auth.isLoggedInNoAnonymousCheck, auth.can("view
|
|
|
222
222
|
router.post("/", isAuthenticated, async function (req, res) {
|
|
223
223
|
try {
|
|
224
224
|
const s3 = new aws.S3();
|
|
225
|
-
//
|
|
226
|
-
const
|
|
225
|
+
// 1) Check if the file name ends with ".gif"
|
|
226
|
+
const isGifFilename = (filename) => {
|
|
227
|
+
const lowerCaseFilename = filename.toLowerCase();
|
|
228
|
+
console.log("filename===========>", filename);
|
|
229
|
+
console.log("lowerCaseFilename===========>", lowerCaseFilename);
|
|
230
|
+
return lowerCaseFilename.endsWith(".gif");
|
|
231
|
+
};
|
|
232
|
+
// 2) Create the storage with a Key callback that uses 'isGifFilename'
|
|
227
233
|
const storage = s3Storage({
|
|
228
234
|
Key: (req, file, cb) => {
|
|
229
235
|
crypto.pseudoRandomBytes(16, (err, raw) => {
|
|
230
|
-
|
|
236
|
+
if (err)
|
|
237
|
+
return cb(err);
|
|
238
|
+
const rawHex = raw.toString("hex");
|
|
239
|
+
console.log("file.originalname", file.originalname);
|
|
240
|
+
const isGif = isGifFilename(file.originalname);
|
|
241
|
+
console.log("gif===========>", isGif);
|
|
242
|
+
file.outputFormat = isGif ? "gif" : "png";
|
|
243
|
+
cb(null, `${rawHex}.${isGif ? "gif" : "png"}`);
|
|
231
244
|
});
|
|
232
245
|
},
|
|
233
246
|
s3,
|
|
234
247
|
Bucket: process.env.S3_BUCKET,
|
|
235
248
|
multiple: true,
|
|
236
249
|
resize: models.Image.getSharpVersions(req.query.itemType),
|
|
237
|
-
toFormat:
|
|
250
|
+
toFormat: "png",
|
|
238
251
|
});
|
|
239
|
-
|
|
252
|
+
// 3) Allowed MIME types that Sharp commonly supports
|
|
253
|
+
// (Adjust or expand as needed)
|
|
254
|
+
const allowedMimeTypes = [
|
|
255
|
+
"image/png",
|
|
256
|
+
"image/jpeg",
|
|
257
|
+
"image/jpg",
|
|
258
|
+
"image/gif",
|
|
259
|
+
"image/webp",
|
|
260
|
+
"image/tiff",
|
|
261
|
+
"image/svg+xml"
|
|
262
|
+
];
|
|
263
|
+
// 4) A fileFilter to reject non-image or invalid Sharp formats
|
|
264
|
+
const fileFilter = (req, file, cb) => {
|
|
265
|
+
console.log("file------------>", file);
|
|
266
|
+
// Check that it's an image and specifically in our allowed list
|
|
267
|
+
if (!allowedMimeTypes.includes(file.mimetype.toLowerCase())) {
|
|
268
|
+
return cb(new Error(`Unsupported file type: ${file.mimetype}. Allowed: ${allowedMimeTypes.join(", ")}`));
|
|
269
|
+
}
|
|
270
|
+
cb(null, true);
|
|
271
|
+
};
|
|
272
|
+
// 5) Construct multer with:
|
|
273
|
+
// - the custom S3-based storage
|
|
274
|
+
// - our fileFilter for validation
|
|
275
|
+
// - a file size limit of 50MB (you can adjust as needed)
|
|
276
|
+
const upload = multer({
|
|
277
|
+
storage,
|
|
278
|
+
fileFilter,
|
|
279
|
+
limits: { fileSize: 50 * 1024 * 1024 } // 50MB limit
|
|
280
|
+
});
|
|
281
|
+
// 6) Use upload.single, handle the success/error callback carefully
|
|
240
282
|
upload.single("file")(req, res, async function (error) {
|
|
241
283
|
if (error) {
|
|
242
|
-
|
|
284
|
+
// Multer will throw if file is too large or invalid
|
|
285
|
+
console.error("File upload error:", error);
|
|
286
|
+
return res.status(400).json({ error: error.message });
|
|
243
287
|
}
|
|
244
|
-
|
|
288
|
+
// Continue if there's a valid image file
|
|
289
|
+
try {
|
|
245
290
|
const formats = JSON.stringify(models.Image.createFormatsFromSharpFile(req.file));
|
|
246
291
|
const image = models.Image.build({
|
|
247
292
|
user_id: req.user.id,
|
|
@@ -257,12 +302,21 @@ router.post("/", isAuthenticated, async function (req, res) {
|
|
|
257
302
|
context: "create",
|
|
258
303
|
userId: req.user ? req.user.id : -1,
|
|
259
304
|
});
|
|
260
|
-
res.send(image);
|
|
305
|
+
return res.send(image);
|
|
306
|
+
}
|
|
307
|
+
catch (err) {
|
|
308
|
+
console.error("Error saving image record:", err);
|
|
309
|
+
return res.status(500).json({ error: "Failed to save image record" });
|
|
261
310
|
}
|
|
262
311
|
});
|
|
263
312
|
}
|
|
264
|
-
catch (
|
|
265
|
-
|
|
313
|
+
catch (err) {
|
|
314
|
+
console.error("Unexpected error:", err);
|
|
315
|
+
// If req.file exists, use its data; otherwise fallback
|
|
316
|
+
const fileName = req.file ? req.file.originalname : "unknown filename";
|
|
317
|
+
return res.status(500).json({
|
|
318
|
+
error: `Unhandled error while processing '${fileName}'`,
|
|
319
|
+
});
|
|
266
320
|
}
|
|
267
321
|
});
|
|
268
322
|
// Post User Images
|
package/controllers/nonSpa.cjs
CHANGED
|
@@ -554,7 +554,7 @@ router.get('/*', function botController(req, res, next) {
|
|
|
554
554
|
], error => {
|
|
555
555
|
if (error) {
|
|
556
556
|
log.error("Id for nonSpa is not a number", { error });
|
|
557
|
-
res.sendStatus(
|
|
557
|
+
res.sendStatus(200);
|
|
558
558
|
}
|
|
559
559
|
else {
|
|
560
560
|
if (req.ypCommunity && req.ypCommunity.id != null) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yrpri/api",
|
|
3
|
-
"version": "9.0.
|
|
3
|
+
"version": "9.0.91",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Robert Bjarnason & Citizens Foundation",
|
|
6
6
|
"repository": {
|
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
"sanitize-filename": "^1.6.3",
|
|
95
95
|
"sequelize": "^6.36.0",
|
|
96
96
|
"sequelize-cli": "^6.2.0",
|
|
97
|
-
"sharp": "^0.33.
|
|
97
|
+
"sharp": "^0.33.5",
|
|
98
98
|
"sitemap": "^7.1.2",
|
|
99
99
|
"socket.io": "^4.0.0",
|
|
100
100
|
"stripe": "^17.3.0",
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const models = require('../../models/index.cjs');
|
|
3
|
+
const moment = require('moment');
|
|
4
|
+
const maxNumberFromPath = process.argv[2];
|
|
5
|
+
const maxNumberOfNotificationsToDelete = maxNumberFromPath ? maxNumberFromPath : 1000;
|
|
6
|
+
let numberOfDeletedNotifications = 0;
|
|
7
|
+
let startTime = moment();
|
|
8
|
+
function sleep(ms) {
|
|
9
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
10
|
+
}
|
|
11
|
+
const chunk = (arr, size) => Array.from({ length: Math.ceil(arr.length / size) }, (v, i) => arr.slice(i * size, i * size + size));
|
|
12
|
+
(async () => {
|
|
13
|
+
let haveNotificationsToDelete = true;
|
|
14
|
+
let userOffset = 0;
|
|
15
|
+
while (haveNotificationsToDelete && numberOfDeletedNotifications < maxNumberOfNotificationsToDelete) {
|
|
16
|
+
try {
|
|
17
|
+
const users = await models.User.unscoped().findAll({
|
|
18
|
+
where: {
|
|
19
|
+
created_at: {
|
|
20
|
+
[models.Sequelize.Op.lte]: moment().add(-3, 'days').toISOString()
|
|
21
|
+
},
|
|
22
|
+
profile_data: {
|
|
23
|
+
isAnonymousUser: {
|
|
24
|
+
[models.Sequelize.Op.is]: true
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
attributes: ['id'],
|
|
29
|
+
order: ['id'],
|
|
30
|
+
offset: userOffset,
|
|
31
|
+
limit: 500
|
|
32
|
+
});
|
|
33
|
+
if (users.length > 0) {
|
|
34
|
+
console.log(`${users.length} users offset ${userOffset}`);
|
|
35
|
+
userOffset += 500;
|
|
36
|
+
const userIds = users.map(n => { return n.id; });
|
|
37
|
+
let haveNotificationsLeftToProcess = true;
|
|
38
|
+
let notificationsOffset = 0;
|
|
39
|
+
while (haveNotificationsLeftToProcess && numberOfDeletedNotifications < maxNumberOfNotificationsToDelete) {
|
|
40
|
+
const notifications = await models.AcNotification.unscoped().findAll({
|
|
41
|
+
where: {
|
|
42
|
+
user_id: {
|
|
43
|
+
[models.Sequelize.Op.in]: userIds
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
limit: 1000,
|
|
47
|
+
offset: notificationsOffset,
|
|
48
|
+
order: ['user_id'],
|
|
49
|
+
attributes: ['id'],
|
|
50
|
+
});
|
|
51
|
+
console.log(`${notifications.length} notifications offset ${notificationsOffset}`);
|
|
52
|
+
if (notifications.length > 0) {
|
|
53
|
+
notificationsOffset += 1000;
|
|
54
|
+
const notificationIds = notifications.map(n => { return n.id; });
|
|
55
|
+
const chunkedIds = chunk(notificationIds, 100);
|
|
56
|
+
for (let i = 0; i < chunkedIds.length; i++) {
|
|
57
|
+
const destroyInfo = await models.AcNotification.unscoped().destroy({
|
|
58
|
+
where: {
|
|
59
|
+
id: {
|
|
60
|
+
[models.Sequelize.Op.in]: chunkedIds[i]
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
numberOfDeletedNotifications += destroyInfo;
|
|
65
|
+
console.log(`${numberOfDeletedNotifications}`);
|
|
66
|
+
await sleep(50);
|
|
67
|
+
if (numberOfDeletedNotifications >= maxNumberOfNotificationsToDelete) {
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
haveNotificationsLeftToProcess = false;
|
|
74
|
+
console.log("No more notifications left to process from user");
|
|
75
|
+
}
|
|
76
|
+
await sleep(100);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
haveNotificationsToDelete = false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
console.error(error);
|
|
85
|
+
haveNotificationsToDelete = false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
console.log(`${numberOfDeletedNotifications} old anon notifications deleted`);
|
|
89
|
+
console.log(`Duration ${moment(moment() - startTime).format("HH:mm:ss.SSS")}`);
|
|
90
|
+
process.exit();
|
|
91
|
+
})();
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const models = require('../../models/index.cjs');
|
|
3
|
+
const moment = require('moment');
|
|
4
|
+
const maxNumberFromPath = process.argv[2];
|
|
5
|
+
// Default to 1,000,000 if no command line arg given, adjust as you see fit
|
|
6
|
+
const maxNumberOfNotificationsToDelete = maxNumberFromPath ? parseInt(maxNumberFromPath) : 1000000;
|
|
7
|
+
let numberOfDeletedNotifications = 0;
|
|
8
|
+
const startTime = moment();
|
|
9
|
+
function sleep(ms) {
|
|
10
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
11
|
+
}
|
|
12
|
+
function chunk(arr, size) {
|
|
13
|
+
return Array.from({ length: Math.ceil(arr.length / size) }, (_, i) => arr.slice(i * size, i * size + size));
|
|
14
|
+
}
|
|
15
|
+
(async () => {
|
|
16
|
+
let haveNotificationsToDelete = true;
|
|
17
|
+
let offset = 0;
|
|
18
|
+
// Calculate the cutoff date for "older than 1 year"
|
|
19
|
+
const cutoffDate = moment().subtract(1, 'year').toISOString();
|
|
20
|
+
while (haveNotificationsToDelete && numberOfDeletedNotifications < maxNumberOfNotificationsToDelete) {
|
|
21
|
+
try {
|
|
22
|
+
// Fetch a batch of notifications older than 1 year
|
|
23
|
+
const notifications = await models.AcNotification.unscoped().findAll({
|
|
24
|
+
where: {
|
|
25
|
+
created_at: {
|
|
26
|
+
[models.Sequelize.Op.lte]: cutoffDate,
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
limit: 1000,
|
|
30
|
+
offset: offset,
|
|
31
|
+
order: ['id'],
|
|
32
|
+
attributes: ['id'],
|
|
33
|
+
});
|
|
34
|
+
if (notifications.length > 0) {
|
|
35
|
+
console.log(`${notifications.length} notifications found at offset ${offset}`);
|
|
36
|
+
offset += 1000;
|
|
37
|
+
// Extract IDs and chunk them to avoid huge deletions in one query
|
|
38
|
+
const notificationIds = notifications.map(n => n.id);
|
|
39
|
+
const chunkedIds = chunk(notificationIds, 100);
|
|
40
|
+
// Delete chunk by chunk
|
|
41
|
+
for (const chunkIds of chunkedIds) {
|
|
42
|
+
const destroyInfo = await models.AcNotification.unscoped().destroy({
|
|
43
|
+
where: {
|
|
44
|
+
id: {
|
|
45
|
+
[models.Sequelize.Op.in]: chunkIds,
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
numberOfDeletedNotifications += destroyInfo;
|
|
50
|
+
console.log(`Total deleted so far: ${numberOfDeletedNotifications}`);
|
|
51
|
+
await sleep(10); // short pause
|
|
52
|
+
// Stop if we’ve hit our daily (or run) limit
|
|
53
|
+
if (numberOfDeletedNotifications >= maxNumberOfNotificationsToDelete) {
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
// No more notifications to delete
|
|
60
|
+
haveNotificationsToDelete = false;
|
|
61
|
+
}
|
|
62
|
+
await sleep(25); // short pause between big fetches
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
console.error(error);
|
|
66
|
+
haveNotificationsToDelete = false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
console.log(`${numberOfDeletedNotifications} old notifications deleted`);
|
|
70
|
+
console.log(`Duration: ${moment(moment() - startTime).format('HH:mm:ss.SSS')}`);
|
|
71
|
+
process.exit();
|
|
72
|
+
})();
|
|
@@ -68,6 +68,10 @@ var generateSitemap = async function (req, res) {
|
|
|
68
68
|
})
|
|
69
69
|
.then(function (communities) {
|
|
70
70
|
_.forEach(communities, function (community) {
|
|
71
|
+
if (!community) {
|
|
72
|
+
console.error("No community found in sitemap generation");
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
71
75
|
const path = "/community/" + community.id;
|
|
72
76
|
if (community.hostname &&
|
|
73
77
|
wildCardDomainNames.indexOf(domainName) > -1) {
|
|
@@ -134,6 +138,10 @@ var generateSitemap = async function (req, res) {
|
|
|
134
138
|
})
|
|
135
139
|
.then(function (groups) {
|
|
136
140
|
_.forEach(groups, function (group) {
|
|
141
|
+
if (!group) {
|
|
142
|
+
console.error("No group found in sitemap generation");
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
137
145
|
const path = "/group/" + group.id;
|
|
138
146
|
if (group.Community.hostname &&
|
|
139
147
|
wildCardDomainNames.indexOf(domainName) > -1) {
|
|
@@ -217,6 +225,10 @@ var generateSitemap = async function (req, res) {
|
|
|
217
225
|
})
|
|
218
226
|
.then(function (posts) {
|
|
219
227
|
_.forEach(posts, function (post) {
|
|
228
|
+
if (!post) {
|
|
229
|
+
console.error("No post found in sitemap generation");
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
220
232
|
links.push({ url: "/post/" + post.id });
|
|
221
233
|
});
|
|
222
234
|
seriesCallback();
|