kythia-core 0.11.0-beta → 0.11.1-beta
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 +1 -1
- package/src/Kythia.js +1 -1
- package/src/KythiaClient.js +1 -2
- package/src/cli/Command.js +1 -1
- package/src/cli/commands/CacheClearCommand.js +1 -1
- package/src/cli/commands/LangCheckCommand.js +51 -22
- package/src/cli/commands/LangTranslateCommand.js +1 -1
- package/src/cli/commands/MakeMigrationCommand.js +1 -1
- package/src/cli/commands/MakeModelCommand.js +1 -1
- package/src/cli/commands/MigrateCommand.js +1 -1
- package/src/cli/commands/NamespaceCommand.js +1 -1
- package/src/cli/commands/StructureCommand.js +1 -1
- package/src/cli/commands/UpversionCommand.js +1 -1
- package/src/cli/index.js +1 -1
- package/src/cli/utils/db.js +1 -1
- package/src/database/KythiaMigrator.js +1 -1
- package/src/database/KythiaModel.js +70 -41
- package/src/database/KythiaSequelize.js +1 -1
- package/src/database/KythiaStorage.js +1 -1
- package/src/database/ModelLoader.js +1 -1
- package/src/managers/AddonManager.js +1 -1
- package/src/managers/EventManager.js +1 -1
- package/src/managers/InteractionManager.js +55 -1
- package/src/managers/ShutdownManager.js +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kythia-core",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.1-beta",
|
|
4
4
|
"description": "Core library for the Kythia main Discord bot: extensible, modular, and scalable foundation for commands, components, and event management.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
package/src/Kythia.js
CHANGED
package/src/KythiaClient.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* @file src/client/kythiaClient.js
|
|
5
5
|
* @copyright © 2025 kenndeclouv
|
|
6
6
|
* @assistant chaa & graa
|
|
7
|
-
* @version 0.11.
|
|
7
|
+
* @version 0.11.1-beta
|
|
8
8
|
*
|
|
9
9
|
* @description
|
|
10
10
|
* Factory function that initializes the Discord.js Client with high-performance
|
|
@@ -49,7 +49,6 @@ module.exports = function kythiaClient() {
|
|
|
49
49
|
|
|
50
50
|
ThreadManager: {
|
|
51
51
|
maxSize: 25,
|
|
52
|
-
keepOverLimit: (thread) => thread.isActive(),
|
|
53
52
|
},
|
|
54
53
|
|
|
55
54
|
GuildMemberManager: {
|
package/src/cli/Command.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* @file src/cli/commands/LangCheckCommand.js
|
|
5
5
|
* @copyright © 2025 kenndeclouv
|
|
6
6
|
* @assistant chaa & graa
|
|
7
|
-
* @version 0.11.
|
|
7
|
+
* @version 0.11.1-beta
|
|
8
8
|
*
|
|
9
9
|
* @description
|
|
10
10
|
* Performs a deep AST analysis of the codebase to find `t()` translation function calls.
|
|
@@ -23,7 +23,25 @@ const glob = require('glob');
|
|
|
23
23
|
const parser = require('@babel/parser');
|
|
24
24
|
const traverse = require('@babel/traverse').default;
|
|
25
25
|
|
|
26
|
-
function
|
|
26
|
+
function deepMerge(target, source) {
|
|
27
|
+
if (typeof target !== 'object' || target === null) return source;
|
|
28
|
+
if (typeof source !== 'object' || source === null) return source;
|
|
29
|
+
|
|
30
|
+
for (const key of Object.keys(source)) {
|
|
31
|
+
if (
|
|
32
|
+
source[key] instanceof Object &&
|
|
33
|
+
target[key] instanceof Object &&
|
|
34
|
+
!Array.isArray(source[key])
|
|
35
|
+
) {
|
|
36
|
+
target[key] = deepMerge(target[key], source[key]);
|
|
37
|
+
} else {
|
|
38
|
+
target[key] = source[key];
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return target;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function getAllKeys(obj, allDefinedKeys, prefix = '') {
|
|
27
45
|
Object.keys(obj).forEach((key) => {
|
|
28
46
|
if (key === '_value' || key === 'text') {
|
|
29
47
|
if (Object.keys(obj).length === 1) return;
|
|
@@ -33,7 +51,7 @@ function getAllKeys(obj, prefix = '') {
|
|
|
33
51
|
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
34
52
|
if (typeof obj[key] === 'object' && obj[key] !== null) {
|
|
35
53
|
if (key !== 'jobs' && key !== 'shop') {
|
|
36
|
-
getAllKeys(obj[key], fullKey);
|
|
54
|
+
getAllKeys(obj[key], allDefinedKeys, fullKey);
|
|
37
55
|
} else {
|
|
38
56
|
allDefinedKeys.add(fullKey);
|
|
39
57
|
}
|
|
@@ -49,9 +67,8 @@ class LangCheckCommand extends Command {
|
|
|
49
67
|
'Lint translation key usage in code and language files (AST-based)';
|
|
50
68
|
|
|
51
69
|
async handle() {
|
|
52
|
-
const PROJECT_ROOT =
|
|
70
|
+
const PROJECT_ROOT = process.cwd();
|
|
53
71
|
const SCAN_DIRECTORIES = ['addons', 'src'];
|
|
54
|
-
const LANG_DIR = path.join(PROJECT_ROOT, 'src', 'lang');
|
|
55
72
|
const DEFAULT_LANG = 'en';
|
|
56
73
|
const IGNORE_PATTERNS = [
|
|
57
74
|
'**/node_modules/**',
|
|
@@ -94,41 +111,53 @@ class LangCheckCommand extends Command {
|
|
|
94
111
|
}
|
|
95
112
|
|
|
96
113
|
function _loadLocales() {
|
|
97
|
-
console.log(`\n🔍
|
|
114
|
+
console.log(`\n🔍 Searching for language files in: ${PROJECT_ROOT}`);
|
|
98
115
|
try {
|
|
99
|
-
const langFiles =
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
!file.includes('_FLAT'),
|
|
106
|
-
);
|
|
116
|
+
const langFiles = glob.sync('**/lang/*.json', {
|
|
117
|
+
cwd: PROJECT_ROOT,
|
|
118
|
+
ignore: ['**/node_modules/**', '**/dist/**'],
|
|
119
|
+
absolute: true,
|
|
120
|
+
});
|
|
121
|
+
|
|
107
122
|
if (langFiles.length === 0) {
|
|
108
123
|
console.error(
|
|
109
124
|
'\x1b[31m%s\x1b[0m',
|
|
110
|
-
'❌ No .json files found in
|
|
125
|
+
'❌ No .json files found in any lang folder.',
|
|
111
126
|
);
|
|
112
127
|
return false;
|
|
113
128
|
}
|
|
129
|
+
|
|
130
|
+
let loadedCount = 0;
|
|
114
131
|
for (const file of langFiles) {
|
|
115
|
-
|
|
116
|
-
|
|
132
|
+
if (file.includes('_flat') || file.includes('_FLAT')) continue;
|
|
133
|
+
|
|
134
|
+
const filename = path.basename(file);
|
|
135
|
+
const lang = filename.replace('.json', '');
|
|
136
|
+
const content = fs.readFileSync(file, 'utf8');
|
|
137
|
+
|
|
117
138
|
try {
|
|
118
|
-
|
|
119
|
-
|
|
139
|
+
const parsed = JSON.parse(content);
|
|
140
|
+
if (!locales[lang]) {
|
|
141
|
+
locales[lang] = parsed;
|
|
142
|
+
} else {
|
|
143
|
+
// Merge with existing locale data
|
|
144
|
+
locales[lang] = deepMerge(locales[lang], parsed);
|
|
145
|
+
}
|
|
146
|
+
loadedCount++;
|
|
120
147
|
} catch (jsonError) {
|
|
121
148
|
console.error(
|
|
122
149
|
`\x1b[31m%s\x1b[0m`,
|
|
123
|
-
`❌ Failed to parse JSON: ${file} - ${jsonError.message}`,
|
|
150
|
+
`❌ Failed to parse JSON: ${path.relative(PROJECT_ROOT, file)} - ${jsonError.message}`,
|
|
124
151
|
);
|
|
125
152
|
filesWithErrors++;
|
|
126
153
|
}
|
|
127
154
|
}
|
|
155
|
+
console.log(` > Successfully loaded ${loadedCount} language files.`);
|
|
156
|
+
|
|
128
157
|
if (!locales[DEFAULT_LANG]) {
|
|
129
158
|
console.error(
|
|
130
159
|
`\x1b[31m%s\x1b[0m`,
|
|
131
|
-
`❌ Default language (${DEFAULT_LANG}) not found!`,
|
|
160
|
+
`❌ Default language (${DEFAULT_LANG}) not found in any loaded files!`,
|
|
132
161
|
);
|
|
133
162
|
return false;
|
|
134
163
|
}
|
|
@@ -303,7 +332,7 @@ class LangCheckCommand extends Command {
|
|
|
303
332
|
|
|
304
333
|
if (defaultLocale) {
|
|
305
334
|
try {
|
|
306
|
-
getAllKeys(defaultLocale);
|
|
335
|
+
getAllKeys(defaultLocale, allDefinedKeys);
|
|
307
336
|
} catch (e) {
|
|
308
337
|
console.error('Error collecting defined keys:', e);
|
|
309
338
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* @file src/cli/commands/LangTranslateCommand.js
|
|
5
5
|
* @copyright © 2025 kenndeclouv
|
|
6
6
|
* @assistant chaa & graa
|
|
7
|
-
* @version 0.11.
|
|
7
|
+
* @version 0.11.1-beta
|
|
8
8
|
*
|
|
9
9
|
* @description
|
|
10
10
|
* Automates the translation of the core language file (`en.json`) to a target language
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* @file src/cli/commands/MakeMigrationCommand.js
|
|
5
5
|
* @copyright © 2025 kenndeclouv
|
|
6
6
|
* @assistant chaa & graa
|
|
7
|
-
* @version 0.11.
|
|
7
|
+
* @version 0.11.1-beta
|
|
8
8
|
*
|
|
9
9
|
* @description
|
|
10
10
|
* Scaffolds a new database migration file with a precise YYYYMMDD_HHMMSS timestamp prefix.
|
package/src/cli/index.js
CHANGED
package/src/cli/utils/db.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* @file src/database/KythiaModel.js
|
|
5
5
|
* @copyright © 2025 kenndeclouv
|
|
6
6
|
* @assistant chaa & graa
|
|
7
|
-
* @version 0.11.
|
|
7
|
+
* @version 0.11.1-beta
|
|
8
8
|
*
|
|
9
9
|
* @description
|
|
10
10
|
* Caching layer for Sequelize Models, now sharding-aware. When config.db.redis.shard === true,
|
|
@@ -659,10 +659,6 @@ class KythiaModel extends Model {
|
|
|
659
659
|
* @param {string|Object} queryIdentifier - A unique string or a Sequelize query object.
|
|
660
660
|
* @returns {string} The final cache key, prefixed with the model's name (e.g., "User:{\"id\":1}").
|
|
661
661
|
*/
|
|
662
|
-
/**
|
|
663
|
-
* 🔑 Generates a consistent, model-specific cache key from a query identifier.
|
|
664
|
-
* FIXED: Handle BigInt in json-stable-stringify
|
|
665
|
-
*/
|
|
666
662
|
static getCacheKey(queryIdentifier) {
|
|
667
663
|
let dataToHash = queryIdentifier;
|
|
668
664
|
|
|
@@ -676,7 +672,7 @@ class KythiaModel extends Model {
|
|
|
676
672
|
}
|
|
677
673
|
|
|
678
674
|
const opts = {
|
|
679
|
-
replacer: (value) =>
|
|
675
|
+
replacer: (_key, value) =>
|
|
680
676
|
typeof value === 'bigint' ? value.toString() : value,
|
|
681
677
|
};
|
|
682
678
|
|
|
@@ -712,6 +708,38 @@ class KythiaModel extends Model {
|
|
|
712
708
|
return normalized;
|
|
713
709
|
}
|
|
714
710
|
|
|
711
|
+
/**
|
|
712
|
+
* 🧠 Generate tags based on PK and static cacheKeys definition.
|
|
713
|
+
* Used for smart invalidation (e.g. invalidate all items belonging to a userId).
|
|
714
|
+
*/
|
|
715
|
+
static _generateSmartTags(instance) {
|
|
716
|
+
if (!instance) return [`${this.name}`];
|
|
717
|
+
|
|
718
|
+
const tags = [`${this.name}`];
|
|
719
|
+
|
|
720
|
+
const pk = this.primaryKeyAttribute;
|
|
721
|
+
if (instance[pk]) {
|
|
722
|
+
tags.push(`${this.name}:${pk}:${instance[pk]}`);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
const smartKeys = this.cacheKeys || this.CACHE_KEYS || [];
|
|
726
|
+
|
|
727
|
+
if (Array.isArray(smartKeys)) {
|
|
728
|
+
for (const keyGroup of smartKeys) {
|
|
729
|
+
const keys = Array.isArray(keyGroup) ? keyGroup : [keyGroup];
|
|
730
|
+
const hasAllValues = keys.every(
|
|
731
|
+
(k) => instance[k] !== undefined && instance[k] !== null,
|
|
732
|
+
);
|
|
733
|
+
|
|
734
|
+
if (hasAllValues) {
|
|
735
|
+
const tagParts = keys.map((k) => `${k}:${instance[k]}`).join(':');
|
|
736
|
+
tags.push(`${this.name}:${tagParts}`);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
return tags;
|
|
741
|
+
}
|
|
742
|
+
|
|
715
743
|
/**
|
|
716
744
|
* 📥 [HYBRID/SHARD ROUTER] Sets a value in the currently active cache engine.
|
|
717
745
|
* In shard mode, if Redis down, nothing is cached.
|
|
@@ -774,6 +802,7 @@ class KythiaModel extends Model {
|
|
|
774
802
|
static async _redisSetCacheEntry(cacheKey, data, ttl, tags = []) {
|
|
775
803
|
try {
|
|
776
804
|
let plainData = data;
|
|
805
|
+
|
|
777
806
|
if (data && typeof data.toJSON === 'function') {
|
|
778
807
|
plainData = data.toJSON();
|
|
779
808
|
} else if (Array.isArray(data)) {
|
|
@@ -789,6 +818,7 @@ class KythiaModel extends Model {
|
|
|
789
818
|
|
|
790
819
|
const multi = this.redis.multi();
|
|
791
820
|
multi.set(cacheKey, valueToStore, 'PX', ttl);
|
|
821
|
+
|
|
792
822
|
for (const tag of tags) {
|
|
793
823
|
multi.sadd(tag, cacheKey);
|
|
794
824
|
}
|
|
@@ -821,19 +851,21 @@ class KythiaModel extends Model {
|
|
|
821
851
|
? Array.isArray(includeOptions)
|
|
822
852
|
? includeOptions
|
|
823
853
|
: [includeOptions]
|
|
824
|
-
:
|
|
854
|
+
: undefined;
|
|
825
855
|
|
|
826
|
-
|
|
827
|
-
const
|
|
856
|
+
const buildInstance = (data) => {
|
|
857
|
+
const instance = this.build(data, {
|
|
828
858
|
isNewRecord: false,
|
|
829
859
|
include: includeAsArray,
|
|
830
860
|
});
|
|
861
|
+
return instance;
|
|
862
|
+
};
|
|
863
|
+
|
|
864
|
+
if (Array.isArray(parsedData)) {
|
|
865
|
+
const instances = parsedData.map((d) => buildInstance(d));
|
|
831
866
|
return { hit: true, data: instances };
|
|
832
867
|
} else {
|
|
833
|
-
const instance =
|
|
834
|
-
isNewRecord: false,
|
|
835
|
-
include: includeAsArray,
|
|
836
|
-
});
|
|
868
|
+
const instance = buildInstance(parsedData);
|
|
837
869
|
return { hit: true, data: instance };
|
|
838
870
|
}
|
|
839
871
|
} catch (err) {
|
|
@@ -1055,14 +1087,6 @@ class KythiaModel extends Model {
|
|
|
1055
1087
|
static async getCache(keys, options = {}) {
|
|
1056
1088
|
const { noCache, customCacheKey, ttl, ...explicitQueryOptions } = options;
|
|
1057
1089
|
|
|
1058
|
-
if (noCache) {
|
|
1059
|
-
const queryToRun = {
|
|
1060
|
-
...this._normalizeFindOptions(keys),
|
|
1061
|
-
...explicitQueryOptions,
|
|
1062
|
-
};
|
|
1063
|
-
return this.findOne(queryToRun);
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
1090
|
if (Array.isArray(keys)) {
|
|
1067
1091
|
const pk = this.primaryKeyAttribute;
|
|
1068
1092
|
return this.findAll({ where: { [pk]: keys.map((m) => m[pk]) } });
|
|
@@ -1074,11 +1098,15 @@ class KythiaModel extends Model {
|
|
|
1074
1098
|
...normalizedKeys,
|
|
1075
1099
|
...explicitQueryOptions,
|
|
1076
1100
|
where: {
|
|
1077
|
-
...normalizedKeys.where,
|
|
1101
|
+
...(normalizedKeys.where || {}),
|
|
1078
1102
|
...(explicitQueryOptions.where || {}),
|
|
1079
1103
|
},
|
|
1080
1104
|
};
|
|
1081
1105
|
|
|
1106
|
+
if (noCache) {
|
|
1107
|
+
return this.findOne(finalQuery);
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1082
1110
|
if (!finalQuery.where || Object.keys(finalQuery.where).length === 0) {
|
|
1083
1111
|
return null;
|
|
1084
1112
|
}
|
|
@@ -1094,7 +1122,7 @@ class KythiaModel extends Model {
|
|
|
1094
1122
|
return this.pendingQueries.get(cacheKey);
|
|
1095
1123
|
|
|
1096
1124
|
const queryPromise = this.findOne(finalQuery)
|
|
1097
|
-
.then((record) => {
|
|
1125
|
+
.then(async (record) => {
|
|
1098
1126
|
if (this.isRedisConnected || !this.isShardMode) {
|
|
1099
1127
|
const tags = [`${this.name}`];
|
|
1100
1128
|
if (record)
|
|
@@ -1104,7 +1132,7 @@ class KythiaModel extends Model {
|
|
|
1104
1132
|
}`,
|
|
1105
1133
|
);
|
|
1106
1134
|
|
|
1107
|
-
this.setCacheEntry(cacheKey, record, ttl, tags);
|
|
1135
|
+
await this.setCacheEntry(cacheKey, record, ttl, tags);
|
|
1108
1136
|
}
|
|
1109
1137
|
return record;
|
|
1110
1138
|
})
|
|
@@ -1142,12 +1170,12 @@ class KythiaModel extends Model {
|
|
|
1142
1170
|
return this.pendingQueries.get(cacheKey);
|
|
1143
1171
|
|
|
1144
1172
|
const queryPromise = this.findAll(normalizedOptions)
|
|
1145
|
-
.then((records) => {
|
|
1173
|
+
.then(async (records) => {
|
|
1146
1174
|
if (this.isRedisConnected || !this.isShardMode) {
|
|
1147
1175
|
const tags = [`${this.name}`];
|
|
1148
1176
|
if (Array.isArray(cacheTags)) tags.push(...cacheTags);
|
|
1149
1177
|
|
|
1150
|
-
this.setCacheEntry(cacheKey, records, ttl, tags);
|
|
1178
|
+
await this.setCacheEntry(cacheKey, records, ttl, tags);
|
|
1151
1179
|
}
|
|
1152
1180
|
return records;
|
|
1153
1181
|
})
|
|
@@ -1340,29 +1368,40 @@ class KythiaModel extends Model {
|
|
|
1340
1368
|
}
|
|
1341
1369
|
|
|
1342
1370
|
/**
|
|
1343
|
-
* 📦
|
|
1344
|
-
*
|
|
1371
|
+
* 📦 FIXED: Save data to DB, then INVALIDATE the cache tags.
|
|
1372
|
+
* Don't try to setCache here, because .save() result doesn't have associations/includes.
|
|
1373
|
+
* Let the next getCache() fetch the full fresh data tree.
|
|
1345
1374
|
*/
|
|
1346
1375
|
async saveAndUpdateCache() {
|
|
1347
1376
|
const savedInstance = await this.save();
|
|
1348
1377
|
const pk = this.constructor.primaryKeyAttribute;
|
|
1349
1378
|
const pkValue = this[pk];
|
|
1379
|
+
|
|
1350
1380
|
if (
|
|
1351
1381
|
pkValue &&
|
|
1352
1382
|
(this.constructor.isRedisConnected || !this.constructor.isShardMode)
|
|
1353
1383
|
) {
|
|
1354
|
-
const cacheKey = this.constructor.getCacheKey({
|
|
1384
|
+
const cacheKey = this.constructor.getCacheKey({
|
|
1385
|
+
where: { [pk]: pkValue },
|
|
1386
|
+
});
|
|
1387
|
+
|
|
1355
1388
|
const tags = [
|
|
1356
1389
|
`${this.constructor.name}`,
|
|
1357
1390
|
`${this.constructor.name}:${pk}:${pkValue}`,
|
|
1358
1391
|
];
|
|
1392
|
+
|
|
1359
1393
|
await this.constructor.setCacheEntry(
|
|
1360
1394
|
cacheKey,
|
|
1361
1395
|
savedInstance,
|
|
1362
1396
|
undefined,
|
|
1363
1397
|
tags,
|
|
1364
1398
|
);
|
|
1399
|
+
|
|
1400
|
+
this.constructor.logger.info(
|
|
1401
|
+
`🔄 [CACHE] Updated cache for ${this.constructor.name}:${pk}:${pkValue}`,
|
|
1402
|
+
);
|
|
1365
1403
|
}
|
|
1404
|
+
|
|
1366
1405
|
return savedInstance;
|
|
1367
1406
|
}
|
|
1368
1407
|
|
|
@@ -1413,16 +1452,11 @@ class KythiaModel extends Model {
|
|
|
1413
1452
|
return;
|
|
1414
1453
|
}
|
|
1415
1454
|
|
|
1416
|
-
/**
|
|
1417
|
-
* Logika setelah data disimpan (Create atau Update)
|
|
1418
|
-
*/
|
|
1419
1455
|
const afterSaveLogic = async (instance) => {
|
|
1420
1456
|
const modelClass = instance.constructor;
|
|
1421
1457
|
|
|
1422
1458
|
if (modelClass.isRedisConnected) {
|
|
1423
|
-
const tagsToInvalidate =
|
|
1424
|
-
const pk = modelClass.primaryKeyAttribute;
|
|
1425
|
-
tagsToInvalidate.push(`${modelClass.name}:${pk}:${instance[pk]}`);
|
|
1459
|
+
const tagsToInvalidate = modelClass._generateSmartTags(instance);
|
|
1426
1460
|
|
|
1427
1461
|
if (Array.isArray(modelClass.customInvalidationTags)) {
|
|
1428
1462
|
tagsToInvalidate.push(...modelClass.customInvalidationTags);
|
|
@@ -1433,16 +1467,11 @@ class KythiaModel extends Model {
|
|
|
1433
1467
|
}
|
|
1434
1468
|
};
|
|
1435
1469
|
|
|
1436
|
-
/**
|
|
1437
|
-
* Logika setelah data dihapus
|
|
1438
|
-
*/
|
|
1439
1470
|
const afterDestroyLogic = async (instance) => {
|
|
1440
1471
|
const modelClass = instance.constructor;
|
|
1441
1472
|
|
|
1442
1473
|
if (modelClass.isRedisConnected) {
|
|
1443
|
-
const tagsToInvalidate =
|
|
1444
|
-
const pk = modelClass.primaryKeyAttribute;
|
|
1445
|
-
tagsToInvalidate.push(`${modelClass.name}:${pk}:${instance[pk]}`);
|
|
1474
|
+
const tagsToInvalidate = modelClass._generateSmartTags(instance);
|
|
1446
1475
|
|
|
1447
1476
|
if (Array.isArray(modelClass.customInvalidationTags)) {
|
|
1448
1477
|
tagsToInvalidate.push(...modelClass.customInvalidationTags);
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* @file src/managers/InteractionManager.js
|
|
5
5
|
* @copyright © 2025 kenndeclouv
|
|
6
6
|
* @assistant chaa & graa
|
|
7
|
-
* @version 0.11.
|
|
7
|
+
* @version 0.11.1-beta
|
|
8
8
|
*
|
|
9
9
|
* @description
|
|
10
10
|
* Handles all Discord interaction events including slash commands, buttons, modals,
|
|
@@ -56,6 +56,10 @@ class InteractionManager {
|
|
|
56
56
|
this.KythiaVoter = this.models.KythiaVoter;
|
|
57
57
|
this.isTeam = this.helpers.discord.isTeam;
|
|
58
58
|
this.isOwner = this.helpers.discord.isOwner;
|
|
59
|
+
|
|
60
|
+
if (!this.client.restartNoticeCooldowns) {
|
|
61
|
+
this.client.restartNoticeCooldowns = new Collection();
|
|
62
|
+
}
|
|
59
63
|
}
|
|
60
64
|
|
|
61
65
|
/**
|
|
@@ -305,6 +309,8 @@ class InteractionManager {
|
|
|
305
309
|
} else {
|
|
306
310
|
await command.execute(interaction);
|
|
307
311
|
}
|
|
312
|
+
|
|
313
|
+
await this._checkRestartSchedule(interaction);
|
|
308
314
|
} else {
|
|
309
315
|
this.logger.error(
|
|
310
316
|
"Command doesn't have a valid 'execute' function:",
|
|
@@ -637,6 +643,9 @@ class InteractionManager {
|
|
|
637
643
|
if (this.container && !this.container.logger) {
|
|
638
644
|
this.container.logger = this.logger;
|
|
639
645
|
}
|
|
646
|
+
|
|
647
|
+
await this._checkRestartSchedule(interaction);
|
|
648
|
+
|
|
640
649
|
await command.execute(interaction, this.container);
|
|
641
650
|
}
|
|
642
651
|
|
|
@@ -706,6 +715,51 @@ class InteractionManager {
|
|
|
706
715
|
}
|
|
707
716
|
}
|
|
708
717
|
|
|
718
|
+
/**
|
|
719
|
+
* Check for scheduled restart and notify user
|
|
720
|
+
* @private
|
|
721
|
+
*/
|
|
722
|
+
async _checkRestartSchedule(interaction) {
|
|
723
|
+
const restartTs = this.client.kythiaRestartTimestamp;
|
|
724
|
+
|
|
725
|
+
if (!restartTs || interaction.commandName === 'restart') return;
|
|
726
|
+
|
|
727
|
+
const userId = interaction.user.id;
|
|
728
|
+
const cooldowns = this.client.restartNoticeCooldowns;
|
|
729
|
+
const now = Date.now();
|
|
730
|
+
const cooldownTime = 5 * 60 * 1000;
|
|
731
|
+
|
|
732
|
+
if (cooldowns.has(userId)) {
|
|
733
|
+
const lastNotified = cooldowns.get(userId);
|
|
734
|
+
if (now - lastNotified < cooldownTime) {
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
const timeLeft = restartTs - now;
|
|
740
|
+
|
|
741
|
+
if (timeLeft > 0) {
|
|
742
|
+
try {
|
|
743
|
+
const timeString = `<t:${Math.floor(restartTs / 1000)}:R>`;
|
|
744
|
+
// TODO: Add translation
|
|
745
|
+
const msg = `## ⚠️ System Notice\nKythia is scheduled to restart **${timeString}**.`;
|
|
746
|
+
|
|
747
|
+
if (interaction.replied || interaction.deferred) {
|
|
748
|
+
await interaction.followUp({
|
|
749
|
+
content: msg,
|
|
750
|
+
flags: MessageFlags.Ephemeral,
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
cooldowns.set(userId, now);
|
|
754
|
+
|
|
755
|
+
setTimeout(() => cooldowns.delete(userId), cooldownTime);
|
|
756
|
+
}
|
|
757
|
+
} catch (err) {
|
|
758
|
+
this.logger.error(err);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
709
763
|
/**
|
|
710
764
|
* Handle interaction errors
|
|
711
765
|
* @private
|