kythia-core 0.10.1-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/README.md +87 -1
- package/package.json +11 -1
- package/src/Kythia.js +9 -7
- package/src/KythiaClient.js +1 -2
- package/src/cli/Command.js +68 -0
- package/src/cli/commands/CacheClearCommand.js +136 -0
- package/src/cli/commands/LangCheckCommand.js +396 -0
- package/src/cli/commands/LangTranslateCommand.js +336 -0
- package/src/cli/commands/MakeMigrationCommand.js +82 -0
- package/src/cli/commands/MakeModelCommand.js +81 -0
- package/src/cli/commands/MigrateCommand.js +259 -0
- package/src/cli/commands/NamespaceCommand.js +112 -0
- package/src/cli/commands/StructureCommand.js +70 -0
- package/src/cli/commands/UpversionCommand.js +94 -0
- package/src/cli/index.js +69 -0
- package/src/cli/utils/db.js +117 -0
- package/src/database/KythiaMigrator.js +1 -1
- package/src/database/KythiaModel.js +76 -48
- 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 +10 -1
- package/src/managers/EventManager.js +1 -1
- package/src/managers/InteractionManager.js +56 -2
- package/src/managers/ShutdownManager.js +1 -1
- package/.husky/pre-commit +0 -4
- package/biome.json +0 -40
- package/bun.lock +0 -445
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🔌 CLI Database & Migration Bootstrapper
|
|
3
|
+
*
|
|
4
|
+
* @file src/cli/utils/db.js
|
|
5
|
+
* @copyright © 2025 kenndeclouv
|
|
6
|
+
* @assistant chaa & graa
|
|
7
|
+
* @version 0.11.1-beta
|
|
8
|
+
*
|
|
9
|
+
* @description
|
|
10
|
+
* Initializes the database connection and migration engine (Umzug) specifically
|
|
11
|
+
* for CLI operations. It dynamically loads the project configuration and
|
|
12
|
+
* discovers migration files across all addons.
|
|
13
|
+
*
|
|
14
|
+
* ✨ Core Features:
|
|
15
|
+
* - Dynamic Config: Loads `kythia.config.js` from the user's project root.
|
|
16
|
+
* - Auto-Discovery: Scans `addons` directory for migration files.
|
|
17
|
+
* - Pretty Logging: Custom console output using `picocolors` for migration status.
|
|
18
|
+
* - Singleton-like: Exports shared instances of Sequelize and Umzug.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
require('@dotenvx/dotenvx/config');
|
|
22
|
+
const fs = require('node:fs');
|
|
23
|
+
const path = require('node:path');
|
|
24
|
+
const { Umzug } = require('umzug');
|
|
25
|
+
const createSequelizeInstance = require('../../database/KythiaSequelize');
|
|
26
|
+
const KythiaStorage = require('../../database/KythiaStorage');
|
|
27
|
+
const pc = require('picocolors');
|
|
28
|
+
|
|
29
|
+
const configPath = path.resolve(process.cwd(), 'kythia.config.js');
|
|
30
|
+
|
|
31
|
+
if (!fs.existsSync(configPath)) {
|
|
32
|
+
console.error('❌ kythia.config.js not found in root directory!');
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const config = require(configPath);
|
|
37
|
+
|
|
38
|
+
const logger = {
|
|
39
|
+
info: () => {},
|
|
40
|
+
error: console.error,
|
|
41
|
+
debug: () => {},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const sequelize = createSequelizeInstance(config, logger);
|
|
45
|
+
|
|
46
|
+
function getMigrationFiles() {
|
|
47
|
+
const rootDir = process.cwd();
|
|
48
|
+
const addonsDir = path.join(rootDir, 'addons');
|
|
49
|
+
|
|
50
|
+
if (!fs.existsSync(addonsDir)) return [];
|
|
51
|
+
|
|
52
|
+
const migrationFiles = [];
|
|
53
|
+
const addonFolders = fs
|
|
54
|
+
.readdirSync(addonsDir)
|
|
55
|
+
.filter((f) => fs.statSync(path.join(addonsDir, f)).isDirectory());
|
|
56
|
+
|
|
57
|
+
for (const addon of addonFolders) {
|
|
58
|
+
const migrationDir = path.join(addonsDir, addon, 'database', 'migrations');
|
|
59
|
+
if (fs.existsSync(migrationDir)) {
|
|
60
|
+
const files = fs
|
|
61
|
+
.readdirSync(migrationDir)
|
|
62
|
+
.filter((f) => f.endsWith('.js'))
|
|
63
|
+
.map((f) => ({
|
|
64
|
+
name: f,
|
|
65
|
+
path: path.join(migrationDir, f),
|
|
66
|
+
}));
|
|
67
|
+
migrationFiles.push(...files);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return migrationFiles.sort((a, b) => a.name.localeCompare(b.name));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const storage = new KythiaStorage({ sequelize });
|
|
74
|
+
|
|
75
|
+
const umzugLogger = {
|
|
76
|
+
info: (event) => {
|
|
77
|
+
if (typeof event === 'object') {
|
|
78
|
+
if (event.event === 'migrated') {
|
|
79
|
+
console.log(
|
|
80
|
+
pc.green(`✅ Migrated: ${event.name} `) +
|
|
81
|
+
pc.gray(`(${event.durationSeconds}s)`),
|
|
82
|
+
);
|
|
83
|
+
} else if (event.event === 'reverting') {
|
|
84
|
+
console.log(pc.yellow(`↩️ Reverting: ${event.name}`));
|
|
85
|
+
} else if (event.event === 'reverted') {
|
|
86
|
+
console.log(
|
|
87
|
+
pc.red(`❌ Reverted: ${event.name} `) +
|
|
88
|
+
pc.gray(`(${event.durationSeconds}s)`),
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
console.log(pc.dim(event));
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
warn: (msg) => console.warn(pc.yellow(`⚠️ ${msg}`)),
|
|
96
|
+
error: (msg) => console.error(pc.red(`🔥 ${msg}`)),
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const umzug = new Umzug({
|
|
100
|
+
migrations: getMigrationFiles().map((m) => ({
|
|
101
|
+
name: m.name,
|
|
102
|
+
path: m.path,
|
|
103
|
+
up: async ({ context }) => {
|
|
104
|
+
const migration = require(m.path);
|
|
105
|
+
return migration.up(context, require('sequelize').DataTypes);
|
|
106
|
+
},
|
|
107
|
+
down: async ({ context }) => {
|
|
108
|
+
const migration = require(m.path);
|
|
109
|
+
return migration.down(context, require('sequelize').DataTypes);
|
|
110
|
+
},
|
|
111
|
+
})),
|
|
112
|
+
context: sequelize.getQueryInterface(),
|
|
113
|
+
storage: storage,
|
|
114
|
+
logger: umzugLogger,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
module.exports = { sequelize, umzug, storage };
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* @file src/database/KythiaModel.js
|
|
5
5
|
* @copyright © 2025 kenndeclouv
|
|
6
6
|
* @assistant chaa & graa
|
|
7
|
-
* @version 0.
|
|
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,
|
|
@@ -32,7 +32,7 @@ const REDIS_ERROR_TOLERANCE_INTERVAL_MS = 10 * 1000;
|
|
|
32
32
|
|
|
33
33
|
function safeStringify(obj, logger) {
|
|
34
34
|
try {
|
|
35
|
-
return JSON.stringify(obj, (value) =>
|
|
35
|
+
return JSON.stringify(obj, (_key, value) =>
|
|
36
36
|
typeof value === 'bigint' ? value.toString() : value,
|
|
37
37
|
);
|
|
38
38
|
} catch (err) {
|
|
@@ -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
|
}
|
|
@@ -799,42 +829,43 @@ class KythiaModel extends Model {
|
|
|
799
829
|
}
|
|
800
830
|
}
|
|
801
831
|
|
|
802
|
-
/**
|
|
803
|
-
* 🔴 (Private) Retrieves and deserializes an entry specifically from Redis.
|
|
804
|
-
*/
|
|
805
832
|
static async _redisGetCachedEntry(cacheKey, includeOptions) {
|
|
806
833
|
try {
|
|
807
834
|
const result = await this.redis.get(cacheKey);
|
|
835
|
+
|
|
808
836
|
if (result === null || result === undefined)
|
|
809
837
|
return { hit: false, data: undefined };
|
|
810
838
|
|
|
811
839
|
this.cacheStats.redisHits++;
|
|
812
|
-
if (result === NEGATIVE_CACHE_PLACEHOLDER)
|
|
840
|
+
if (result === NEGATIVE_CACHE_PLACEHOLDER) {
|
|
813
841
|
return { hit: true, data: null };
|
|
842
|
+
}
|
|
814
843
|
|
|
815
844
|
const parsedData = safeParse(result, this.logger);
|
|
816
845
|
|
|
817
|
-
if (
|
|
818
|
-
return { hit:
|
|
846
|
+
if (parsedData === null) {
|
|
847
|
+
return { hit: false, data: undefined };
|
|
819
848
|
}
|
|
820
849
|
|
|
821
850
|
const includeAsArray = includeOptions
|
|
822
851
|
? Array.isArray(includeOptions)
|
|
823
852
|
? includeOptions
|
|
824
853
|
: [includeOptions]
|
|
825
|
-
:
|
|
854
|
+
: undefined;
|
|
826
855
|
|
|
827
|
-
|
|
828
|
-
const
|
|
856
|
+
const buildInstance = (data) => {
|
|
857
|
+
const instance = this.build(data, {
|
|
829
858
|
isNewRecord: false,
|
|
830
859
|
include: includeAsArray,
|
|
831
860
|
});
|
|
861
|
+
return instance;
|
|
862
|
+
};
|
|
863
|
+
|
|
864
|
+
if (Array.isArray(parsedData)) {
|
|
865
|
+
const instances = parsedData.map((d) => buildInstance(d));
|
|
832
866
|
return { hit: true, data: instances };
|
|
833
867
|
} else {
|
|
834
|
-
const instance =
|
|
835
|
-
isNewRecord: false,
|
|
836
|
-
include: includeAsArray,
|
|
837
|
-
});
|
|
868
|
+
const instance = buildInstance(parsedData);
|
|
838
869
|
return { hit: true, data: instance };
|
|
839
870
|
}
|
|
840
871
|
} catch (err) {
|
|
@@ -1056,14 +1087,6 @@ class KythiaModel extends Model {
|
|
|
1056
1087
|
static async getCache(keys, options = {}) {
|
|
1057
1088
|
const { noCache, customCacheKey, ttl, ...explicitQueryOptions } = options;
|
|
1058
1089
|
|
|
1059
|
-
if (noCache) {
|
|
1060
|
-
const queryToRun = {
|
|
1061
|
-
...this._normalizeFindOptions(keys),
|
|
1062
|
-
...explicitQueryOptions,
|
|
1063
|
-
};
|
|
1064
|
-
return this.findOne(queryToRun);
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
1090
|
if (Array.isArray(keys)) {
|
|
1068
1091
|
const pk = this.primaryKeyAttribute;
|
|
1069
1092
|
return this.findAll({ where: { [pk]: keys.map((m) => m[pk]) } });
|
|
@@ -1075,11 +1098,15 @@ class KythiaModel extends Model {
|
|
|
1075
1098
|
...normalizedKeys,
|
|
1076
1099
|
...explicitQueryOptions,
|
|
1077
1100
|
where: {
|
|
1078
|
-
...normalizedKeys.where,
|
|
1101
|
+
...(normalizedKeys.where || {}),
|
|
1079
1102
|
...(explicitQueryOptions.where || {}),
|
|
1080
1103
|
},
|
|
1081
1104
|
};
|
|
1082
1105
|
|
|
1106
|
+
if (noCache) {
|
|
1107
|
+
return this.findOne(finalQuery);
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1083
1110
|
if (!finalQuery.where || Object.keys(finalQuery.where).length === 0) {
|
|
1084
1111
|
return null;
|
|
1085
1112
|
}
|
|
@@ -1095,7 +1122,7 @@ class KythiaModel extends Model {
|
|
|
1095
1122
|
return this.pendingQueries.get(cacheKey);
|
|
1096
1123
|
|
|
1097
1124
|
const queryPromise = this.findOne(finalQuery)
|
|
1098
|
-
.then((record) => {
|
|
1125
|
+
.then(async (record) => {
|
|
1099
1126
|
if (this.isRedisConnected || !this.isShardMode) {
|
|
1100
1127
|
const tags = [`${this.name}`];
|
|
1101
1128
|
if (record)
|
|
@@ -1105,7 +1132,7 @@ class KythiaModel extends Model {
|
|
|
1105
1132
|
}`,
|
|
1106
1133
|
);
|
|
1107
1134
|
|
|
1108
|
-
this.setCacheEntry(cacheKey, record, ttl, tags);
|
|
1135
|
+
await this.setCacheEntry(cacheKey, record, ttl, tags);
|
|
1109
1136
|
}
|
|
1110
1137
|
return record;
|
|
1111
1138
|
})
|
|
@@ -1143,12 +1170,12 @@ class KythiaModel extends Model {
|
|
|
1143
1170
|
return this.pendingQueries.get(cacheKey);
|
|
1144
1171
|
|
|
1145
1172
|
const queryPromise = this.findAll(normalizedOptions)
|
|
1146
|
-
.then((records) => {
|
|
1173
|
+
.then(async (records) => {
|
|
1147
1174
|
if (this.isRedisConnected || !this.isShardMode) {
|
|
1148
1175
|
const tags = [`${this.name}`];
|
|
1149
1176
|
if (Array.isArray(cacheTags)) tags.push(...cacheTags);
|
|
1150
1177
|
|
|
1151
|
-
this.setCacheEntry(cacheKey, records, ttl, tags);
|
|
1178
|
+
await this.setCacheEntry(cacheKey, records, ttl, tags);
|
|
1152
1179
|
}
|
|
1153
1180
|
return records;
|
|
1154
1181
|
})
|
|
@@ -1341,29 +1368,40 @@ class KythiaModel extends Model {
|
|
|
1341
1368
|
}
|
|
1342
1369
|
|
|
1343
1370
|
/**
|
|
1344
|
-
* 📦
|
|
1345
|
-
*
|
|
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.
|
|
1346
1374
|
*/
|
|
1347
1375
|
async saveAndUpdateCache() {
|
|
1348
1376
|
const savedInstance = await this.save();
|
|
1349
1377
|
const pk = this.constructor.primaryKeyAttribute;
|
|
1350
1378
|
const pkValue = this[pk];
|
|
1379
|
+
|
|
1351
1380
|
if (
|
|
1352
1381
|
pkValue &&
|
|
1353
1382
|
(this.constructor.isRedisConnected || !this.constructor.isShardMode)
|
|
1354
1383
|
) {
|
|
1355
|
-
const cacheKey = this.constructor.getCacheKey({
|
|
1384
|
+
const cacheKey = this.constructor.getCacheKey({
|
|
1385
|
+
where: { [pk]: pkValue },
|
|
1386
|
+
});
|
|
1387
|
+
|
|
1356
1388
|
const tags = [
|
|
1357
1389
|
`${this.constructor.name}`,
|
|
1358
1390
|
`${this.constructor.name}:${pk}:${pkValue}`,
|
|
1359
1391
|
];
|
|
1392
|
+
|
|
1360
1393
|
await this.constructor.setCacheEntry(
|
|
1361
1394
|
cacheKey,
|
|
1362
1395
|
savedInstance,
|
|
1363
1396
|
undefined,
|
|
1364
1397
|
tags,
|
|
1365
1398
|
);
|
|
1399
|
+
|
|
1400
|
+
this.constructor.logger.info(
|
|
1401
|
+
`🔄 [CACHE] Updated cache for ${this.constructor.name}:${pk}:${pkValue}`,
|
|
1402
|
+
);
|
|
1366
1403
|
}
|
|
1404
|
+
|
|
1367
1405
|
return savedInstance;
|
|
1368
1406
|
}
|
|
1369
1407
|
|
|
@@ -1414,16 +1452,11 @@ class KythiaModel extends Model {
|
|
|
1414
1452
|
return;
|
|
1415
1453
|
}
|
|
1416
1454
|
|
|
1417
|
-
/**
|
|
1418
|
-
* Logika setelah data disimpan (Create atau Update)
|
|
1419
|
-
*/
|
|
1420
1455
|
const afterSaveLogic = async (instance) => {
|
|
1421
1456
|
const modelClass = instance.constructor;
|
|
1422
1457
|
|
|
1423
1458
|
if (modelClass.isRedisConnected) {
|
|
1424
|
-
const tagsToInvalidate =
|
|
1425
|
-
const pk = modelClass.primaryKeyAttribute;
|
|
1426
|
-
tagsToInvalidate.push(`${modelClass.name}:${pk}:${instance[pk]}`);
|
|
1459
|
+
const tagsToInvalidate = modelClass._generateSmartTags(instance);
|
|
1427
1460
|
|
|
1428
1461
|
if (Array.isArray(modelClass.customInvalidationTags)) {
|
|
1429
1462
|
tagsToInvalidate.push(...modelClass.customInvalidationTags);
|
|
@@ -1434,16 +1467,11 @@ class KythiaModel extends Model {
|
|
|
1434
1467
|
}
|
|
1435
1468
|
};
|
|
1436
1469
|
|
|
1437
|
-
/**
|
|
1438
|
-
* Logika setelah data dihapus
|
|
1439
|
-
*/
|
|
1440
1470
|
const afterDestroyLogic = async (instance) => {
|
|
1441
1471
|
const modelClass = instance.constructor;
|
|
1442
1472
|
|
|
1443
1473
|
if (modelClass.isRedisConnected) {
|
|
1444
|
-
const tagsToInvalidate =
|
|
1445
|
-
const pk = modelClass.primaryKeyAttribute;
|
|
1446
|
-
tagsToInvalidate.push(`${modelClass.name}:${pk}:${instance[pk]}`);
|
|
1474
|
+
const tagsToInvalidate = modelClass._generateSmartTags(instance);
|
|
1447
1475
|
|
|
1448
1476
|
if (Array.isArray(modelClass.customInvalidationTags)) {
|
|
1449
1477
|
tagsToInvalidate.push(...modelClass.customInvalidationTags);
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* @file src/managers/AddonManager.js
|
|
5
5
|
* @copyright © 2025 kenndeclouv
|
|
6
6
|
* @assistant chaa & graa
|
|
7
|
-
* @version 0.
|
|
7
|
+
* @version 0.11.1-beta
|
|
8
8
|
*
|
|
9
9
|
* @description
|
|
10
10
|
* Handles all addon loading, command registration, and component management.
|
|
@@ -466,6 +466,7 @@ class AddonManager {
|
|
|
466
466
|
async _loadTopLevelCommandGroup(
|
|
467
467
|
commandsPath,
|
|
468
468
|
addon,
|
|
469
|
+
addonPermissionDefaults,
|
|
469
470
|
commandNamesSet,
|
|
470
471
|
commandDataForDeployment,
|
|
471
472
|
) {
|
|
@@ -476,6 +477,14 @@ class AddonManager {
|
|
|
476
477
|
commandDef = this._instantiateBaseCommand(commandDef);
|
|
477
478
|
}
|
|
478
479
|
|
|
480
|
+
const category = addon.name;
|
|
481
|
+
const categoryDefaults = addonPermissionDefaults[category] || {};
|
|
482
|
+
|
|
483
|
+
commandDef = {
|
|
484
|
+
...categoryDefaults,
|
|
485
|
+
...commandDef,
|
|
486
|
+
};
|
|
487
|
+
|
|
479
488
|
const mainBuilder = this._createBuilderFromData(
|
|
480
489
|
commandDef.data,
|
|
481
490
|
SlashCommandBuilder,
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* @file src/managers/InteractionManager.js
|
|
5
5
|
* @copyright © 2025 kenndeclouv
|
|
6
6
|
* @assistant chaa & graa
|
|
7
|
-
* @version 0.
|
|
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
|
|
@@ -763,7 +817,7 @@ class InteractionManager {
|
|
|
763
817
|
.setLabel(
|
|
764
818
|
await this.t(interaction, 'common.error.button.contact.owner'),
|
|
765
819
|
)
|
|
766
|
-
.setURL(`discord
|
|
820
|
+
.setURL(`https://discord.com/users/${ownerFirstId}`),
|
|
767
821
|
),
|
|
768
822
|
),
|
|
769
823
|
];
|
package/.husky/pre-commit
DELETED
package/biome.json
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "https://biomejs.dev/schemas/2.3.7/schema.json",
|
|
3
|
-
"vcs": {
|
|
4
|
-
"enabled": true,
|
|
5
|
-
"clientKind": "git",
|
|
6
|
-
"useIgnoreFile": true
|
|
7
|
-
},
|
|
8
|
-
"files": {
|
|
9
|
-
"ignoreUnknown": false
|
|
10
|
-
},
|
|
11
|
-
"formatter": {
|
|
12
|
-
"enabled": true,
|
|
13
|
-
"indentStyle": "tab"
|
|
14
|
-
},
|
|
15
|
-
"linter": {
|
|
16
|
-
"enabled": true,
|
|
17
|
-
"rules": {
|
|
18
|
-
"recommended": true,
|
|
19
|
-
"correctness": {
|
|
20
|
-
"noUnusedVariables": "error"
|
|
21
|
-
},
|
|
22
|
-
"complexity": {
|
|
23
|
-
"noThisInStatic": "off"
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
},
|
|
27
|
-
"javascript": {
|
|
28
|
-
"formatter": {
|
|
29
|
-
"quoteStyle": "single"
|
|
30
|
-
}
|
|
31
|
-
},
|
|
32
|
-
"assist": {
|
|
33
|
-
"enabled": true,
|
|
34
|
-
"actions": {
|
|
35
|
-
"source": {
|
|
36
|
-
"organizeImports": "off"
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}
|