lumisjs 1.0.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.
- package/LICENSE +21 -0
- package/README.md +274 -0
- package/SECURITY.md +160 -0
- package/index.js +90 -0
- package/package.json +68 -0
- package/src/cache/CacheManager.js +393 -0
- package/src/cache/MemoryAdapter.js +259 -0
- package/src/cache/MultiLevelCache.js +362 -0
- package/src/cache/RedisAdapter.js +329 -0
- package/src/cache/SQLiteAdapter.js +280 -0
- package/src/cache/index.js +15 -0
- package/src/client/Client.js +554 -0
- package/src/client/ShardingManager.js +274 -0
- package/src/config/ConfigValidator.js +172 -0
- package/src/config/index.js +7 -0
- package/src/dashboard/DashboardManager.js +362 -0
- package/src/datagen/DataGenerator.js +47 -0
- package/src/datagen/apis/GraphqlMocker.js +16 -0
- package/src/datagen/apis/OpenApiMocker.js +18 -0
- package/src/datagen/cli.js +130 -0
- package/src/datagen/formatters/csv.js +45 -0
- package/src/datagen/formatters/index.js +15 -0
- package/src/datagen/formatters/json.js +33 -0
- package/src/datagen/formatters/mongo.js +22 -0
- package/src/datagen/formatters/sql.js +32 -0
- package/src/datagen/index.js +17 -0
- package/src/datagen/plugin/PluginManager.js +43 -0
- package/src/datagen/plugins/example-plugin.js +21 -0
- package/src/datagen/schema/SchemaGenerator.js +172 -0
- package/src/datagen/schema/loadSchema.js +14 -0
- package/src/datagen/utils/seed.js +26 -0
- package/src/di/ServiceContainer.js +229 -0
- package/src/errors/ErrorCodes.js +110 -0
- package/src/errors/LumisError.js +85 -0
- package/src/game/EconomyManager.js +161 -0
- package/src/game/GameSessionManager.js +76 -0
- package/src/game/GuildManager.js +197 -0
- package/src/game/InventoryManager.js +140 -0
- package/src/game/LevelingSystem.js +194 -0
- package/src/game/MusicManager.js +587 -0
- package/src/health/HealthChecker.js +170 -0
- package/src/health/index.js +7 -0
- package/src/performance/PerformanceMonitor.js +244 -0
- package/src/performance/index.js +7 -0
- package/src/security/InputSanitizer.js +185 -0
- package/src/security/SecurityMiddleware.js +231 -0
- package/src/security/index.js +9 -0
- package/src/shutdown/GracefulShutdown.js +159 -0
- package/src/shutdown/index.js +7 -0
- package/src/utils/StructuredLogger.js +118 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { ErrorCodes, ErrorMessages } = require('./ErrorCodes');
|
|
4
|
+
|
|
5
|
+
class LumisError extends Error {
|
|
6
|
+
|
|
7
|
+
constructor(code, ...args) {
|
|
8
|
+
const message = LumisError._getMessage(code, args);
|
|
9
|
+
super(message);
|
|
10
|
+
|
|
11
|
+
this.name = 'LumisError';
|
|
12
|
+
this.code = code;
|
|
13
|
+
|
|
14
|
+
if (Error.captureStackTrace) {
|
|
15
|
+
Error.captureStackTrace(this, this.constructor);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
static _getMessage(code, args) {
|
|
20
|
+
const msg = ErrorMessages[code];
|
|
21
|
+
if (!msg) return `Unknown error: ${code}`;
|
|
22
|
+
if (typeof msg === 'function') return msg(...args);
|
|
23
|
+
return msg;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
class APIError extends LumisError {
|
|
28
|
+
|
|
29
|
+
constructor(path, error, method, status) {
|
|
30
|
+
super(ErrorCodes.API_ERROR, status, error.message || 'Unknown');
|
|
31
|
+
|
|
32
|
+
this.name = 'APIError';
|
|
33
|
+
this.method = method;
|
|
34
|
+
this.path = path;
|
|
35
|
+
this.httpStatus = status;
|
|
36
|
+
this.requestBody = null;
|
|
37
|
+
|
|
38
|
+
this.discordCode = error.code || 0;
|
|
39
|
+
|
|
40
|
+
this.discordMessage = error.message || 'Unknown';
|
|
41
|
+
|
|
42
|
+
this.errors = error.errors || {};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
toJSON() {
|
|
46
|
+
return {
|
|
47
|
+
name: this.name,
|
|
48
|
+
code: this.code,
|
|
49
|
+
discordCode: this.discordCode,
|
|
50
|
+
message: this.discordMessage,
|
|
51
|
+
method: this.method,
|
|
52
|
+
path: this.path,
|
|
53
|
+
httpStatus: this.httpStatus,
|
|
54
|
+
errors: this.errors,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
class WebSocketError extends LumisError {
|
|
60
|
+
|
|
61
|
+
constructor(code, closeCode, ...args) {
|
|
62
|
+
super(code, ...args);
|
|
63
|
+
this.name = 'WebSocketError';
|
|
64
|
+
this.closeCode = closeCode || null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
class RateLimitError extends LumisError {
|
|
69
|
+
|
|
70
|
+
constructor(path, retryAfter, global = false) {
|
|
71
|
+
super(ErrorCodes.RATE_LIMITED, retryAfter);
|
|
72
|
+
|
|
73
|
+
this.name = 'RateLimitError';
|
|
74
|
+
this.path = path;
|
|
75
|
+
this.retryAfter = retryAfter;
|
|
76
|
+
this.global = global;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
module.exports = {
|
|
81
|
+
LumisError,
|
|
82
|
+
APIError,
|
|
83
|
+
WebSocketError,
|
|
84
|
+
RateLimitError,
|
|
85
|
+
};
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
class EconomyManager {
|
|
4
|
+
/**
|
|
5
|
+
* @param {import('../client/Client')} client
|
|
6
|
+
* @param {object} [options]
|
|
7
|
+
* @param {string} [options.currencyName='Coins']
|
|
8
|
+
* @param {string} [options.currencySymbol='💰']
|
|
9
|
+
* @param {number} [options.defaultBalance=0]
|
|
10
|
+
* @param {number} [options.maxBalance=Infinity]
|
|
11
|
+
*/
|
|
12
|
+
constructor(client, options = {}) {
|
|
13
|
+
this.client = client;
|
|
14
|
+
this.currencyName = options.currencyName || 'Coins';
|
|
15
|
+
this.currencySymbol = options.currencySymbol || '💰';
|
|
16
|
+
this.defaultBalance = options.defaultBalance ?? 0;
|
|
17
|
+
this.maxBalance = options.maxBalance ?? Infinity;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
_key(userId) {
|
|
21
|
+
return `eco_balance_${userId}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get a user's balance.
|
|
26
|
+
* @param {string} userId
|
|
27
|
+
* @returns {Promise<number>}
|
|
28
|
+
*/
|
|
29
|
+
async getBalance(userId) {
|
|
30
|
+
if (!userId) throw new TypeError('userId is required');
|
|
31
|
+
const val = await this.client.cache.get(this._key(userId));
|
|
32
|
+
if (val === null || val === undefined) return this.defaultBalance;
|
|
33
|
+
return Number(val);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Add to a user's balance.
|
|
38
|
+
* @param {string} userId
|
|
39
|
+
* @param {number} amount
|
|
40
|
+
* @returns {Promise<number>} New balance
|
|
41
|
+
*/
|
|
42
|
+
async addBalance(userId, amount) {
|
|
43
|
+
if (!userId) throw new TypeError('userId is required');
|
|
44
|
+
if (typeof amount !== 'number' || isNaN(amount) || amount <= 0) {
|
|
45
|
+
throw new RangeError('amount must be a positive number');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const current = await this.getBalance(userId);
|
|
49
|
+
const newBalance = Math.min(current + amount, this.maxBalance);
|
|
50
|
+
await this.client.cache.set(this._key(userId), newBalance);
|
|
51
|
+
return newBalance;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Remove from a user's balance.
|
|
56
|
+
* @param {string} userId
|
|
57
|
+
* @param {number} amount
|
|
58
|
+
* @returns {Promise<number|false>} New balance, or false if insufficient funds
|
|
59
|
+
*/
|
|
60
|
+
async removeBalance(userId, amount) {
|
|
61
|
+
if (!userId) throw new TypeError('userId is required');
|
|
62
|
+
if (typeof amount !== 'number' || isNaN(amount) || amount <= 0) {
|
|
63
|
+
throw new RangeError('amount must be a positive number');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const current = await this.getBalance(userId);
|
|
67
|
+
if (current < amount) return false;
|
|
68
|
+
|
|
69
|
+
const newBalance = current - amount;
|
|
70
|
+
await this.client.cache.set(this._key(userId), newBalance);
|
|
71
|
+
return newBalance;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Set a user's balance directly.
|
|
76
|
+
* @param {string} userId
|
|
77
|
+
* @param {number} amount
|
|
78
|
+
* @returns {Promise<number>}
|
|
79
|
+
*/
|
|
80
|
+
async setBalance(userId, amount) {
|
|
81
|
+
if (!userId) throw new TypeError('userId is required');
|
|
82
|
+
if (typeof amount !== 'number' || isNaN(amount) || amount < 0) {
|
|
83
|
+
throw new RangeError('amount must be a non-negative number');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const clamped = Math.min(amount, this.maxBalance);
|
|
87
|
+
await this.client.cache.set(this._key(userId), clamped);
|
|
88
|
+
return clamped;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Transfer balance from one user to another.
|
|
93
|
+
* @param {string} fromUserId
|
|
94
|
+
* @param {string} toUserId
|
|
95
|
+
* @param {number} amount
|
|
96
|
+
* @returns {Promise<{ from: number, to: number }|false>}
|
|
97
|
+
*/
|
|
98
|
+
async transfer(fromUserId, toUserId, amount) {
|
|
99
|
+
if (!fromUserId || !toUserId) throw new TypeError('Both user IDs are required');
|
|
100
|
+
if (fromUserId === toUserId) throw new Error('Cannot transfer to the same user');
|
|
101
|
+
if (typeof amount !== 'number' || isNaN(amount) || amount <= 0) {
|
|
102
|
+
throw new RangeError('amount must be a positive number');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const fromBalance = await this.getBalance(fromUserId);
|
|
106
|
+
if (fromBalance < amount) return false;
|
|
107
|
+
|
|
108
|
+
// Sequential to avoid race where both reads happen before writes
|
|
109
|
+
const newFrom = await this.removeBalance(fromUserId, amount);
|
|
110
|
+
const newTo = await this.addBalance(toUserId, amount);
|
|
111
|
+
|
|
112
|
+
return { from: newFrom, to: newTo };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get a leaderboard sorted by balance.
|
|
117
|
+
* Works across all cache adapters via the keys() API.
|
|
118
|
+
* @param {number} [limit=10]
|
|
119
|
+
* @returns {Promise<Array<{ userId: string, balance: number }>>}
|
|
120
|
+
*/
|
|
121
|
+
async getLeaderboard(limit = 10) {
|
|
122
|
+
if (typeof limit !== 'number' || limit < 1) {
|
|
123
|
+
throw new RangeError('limit must be a positive number');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const prefix = 'eco_balance_';
|
|
127
|
+
const users = [];
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const allKeys = await this.client.cache.keys();
|
|
131
|
+
const ecoKeys = allKeys.filter((k) => k.startsWith(prefix));
|
|
132
|
+
|
|
133
|
+
for (const key of ecoKeys) {
|
|
134
|
+
const val = await this.client.cache.get(key);
|
|
135
|
+
if (val !== undefined && val !== null) {
|
|
136
|
+
users.push({
|
|
137
|
+
userId: key.slice(prefix.length),
|
|
138
|
+
balance: Number(val),
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
} catch (err) {
|
|
143
|
+
this.client.logger?.warn(`EconomyManager.getLeaderboard failed: ${err.message}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return users.sort((a, b) => b.balance - a.balance).slice(0, limit);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Reset a user's balance to the default.
|
|
151
|
+
* @param {string} userId
|
|
152
|
+
* @returns {Promise<number>}
|
|
153
|
+
*/
|
|
154
|
+
async resetBalance(userId) {
|
|
155
|
+
if (!userId) throw new TypeError('userId is required');
|
|
156
|
+
await this.client.cache.set(this._key(userId), this.defaultBalance);
|
|
157
|
+
return this.defaultBalance;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
module.exports = EconomyManager;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const Collection = require('../utils/Collection');
|
|
4
|
+
const { EventEmitter } = require('node:events');
|
|
5
|
+
|
|
6
|
+
class GameSessionManager extends EventEmitter {
|
|
7
|
+
constructor() {
|
|
8
|
+
super();
|
|
9
|
+
|
|
10
|
+
this.sessions = new Collection();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
startSession(sessionId, initialData = {}, timeoutMs = 300000) {
|
|
14
|
+
if (this.sessions.has(sessionId)) {
|
|
15
|
+
this.endSession(sessionId, 'force_restart');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const session = {
|
|
19
|
+
id: sessionId,
|
|
20
|
+
data: initialData,
|
|
21
|
+
startTime: Date.now(),
|
|
22
|
+
lastActivityTime: Date.now(),
|
|
23
|
+
timeoutMs,
|
|
24
|
+
_timer: null
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
this.sessions.set(sessionId, session);
|
|
28
|
+
this._resetTimer(session);
|
|
29
|
+
|
|
30
|
+
this.emit('sessionStart', session);
|
|
31
|
+
return session;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
getSession(sessionId) {
|
|
35
|
+
return this.sessions.get(sessionId) || null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
updateSession(sessionId, newData) {
|
|
39
|
+
const session = this.sessions.get(sessionId);
|
|
40
|
+
if (!session) return null;
|
|
41
|
+
|
|
42
|
+
Object.assign(session.data, newData);
|
|
43
|
+
session.lastActivityTime = Date.now();
|
|
44
|
+
|
|
45
|
+
this._resetTimer(session);
|
|
46
|
+
this.emit('sessionUpdate', session);
|
|
47
|
+
return session;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
endSession(sessionId, reason = 'finished') {
|
|
51
|
+
const session = this.sessions.get(sessionId);
|
|
52
|
+
if (!session) return false;
|
|
53
|
+
|
|
54
|
+
if (session._timer) {
|
|
55
|
+
clearTimeout(session._timer);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
this.sessions.delete(sessionId);
|
|
59
|
+
this.emit('sessionEnd', session, reason);
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
_resetTimer(session) {
|
|
64
|
+
if (session._timer) {
|
|
65
|
+
clearTimeout(session._timer);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (session.timeoutMs > 0) {
|
|
69
|
+
session._timer = setTimeout(() => {
|
|
70
|
+
this.endSession(session.id, 'timeout');
|
|
71
|
+
}, session.timeoutMs).unref();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = GameSessionManager;
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { LumisError } = require('../errors/LumisError');
|
|
4
|
+
const { ErrorCodes } = require('../errors/ErrorCodes');
|
|
5
|
+
|
|
6
|
+
class GuildManager {
|
|
7
|
+
/**
|
|
8
|
+
* @param {import('../client/Client')} client
|
|
9
|
+
*/
|
|
10
|
+
constructor(client) {
|
|
11
|
+
this.client = client;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
_nameHistoryKey(userId, guildId) {
|
|
15
|
+
return `name_history_${guildId}_${userId}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Parse JSON from cache safely.
|
|
20
|
+
* @private
|
|
21
|
+
*/
|
|
22
|
+
_parse(raw, fallback) {
|
|
23
|
+
if (!raw) return fallback;
|
|
24
|
+
if (typeof raw === 'object') return raw;
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(raw);
|
|
27
|
+
} catch {
|
|
28
|
+
return fallback;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Register a user in a guild (set nickname + manage roles).
|
|
34
|
+
* @param {object} member
|
|
35
|
+
* @param {string} name
|
|
36
|
+
* @param {string|number} age
|
|
37
|
+
* @param {object} [options]
|
|
38
|
+
* @param {string} [options.nameFormat='{name} | {age}']
|
|
39
|
+
* @param {string[]} [options.rolesToAdd]
|
|
40
|
+
* @param {string[]} [options.rolesToRemove]
|
|
41
|
+
* @returns {Promise<boolean>}
|
|
42
|
+
*/
|
|
43
|
+
async registerUser(member, name, age, options = {}) {
|
|
44
|
+
if (!member?.id || !member?.guildId) {
|
|
45
|
+
throw new LumisError(ErrorCodes.INVALID_STRUCTURE, 'member');
|
|
46
|
+
}
|
|
47
|
+
if (!name || typeof name !== 'string') {
|
|
48
|
+
throw new TypeError('name must be a non-empty string');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const nameFormat = options.nameFormat || '{name} | {age}';
|
|
52
|
+
const formattedName = nameFormat
|
|
53
|
+
.replace('{name}', name.trim())
|
|
54
|
+
.replace('{age}', String(age).trim());
|
|
55
|
+
|
|
56
|
+
// Truncate to Discord's 32-char nickname limit
|
|
57
|
+
const nick = formattedName.slice(0, 32);
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
await this.client.rest.patch(`/guilds/${member.guildId}/members/${member.id}`, {
|
|
61
|
+
body: { nick },
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
if (Array.isArray(options.rolesToAdd)) {
|
|
65
|
+
await Promise.all(
|
|
66
|
+
options.rolesToAdd.map((roleId) =>
|
|
67
|
+
this.client.rest.put(
|
|
68
|
+
`/guilds/${member.guildId}/members/${member.id}/roles/${roleId}`
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (Array.isArray(options.rolesToRemove)) {
|
|
75
|
+
await Promise.all(
|
|
76
|
+
options.rolesToRemove.map((roleId) =>
|
|
77
|
+
this.client.rest.delete(
|
|
78
|
+
`/guilds/${member.guildId}/members/${member.id}/roles/${roleId}`
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
await this.addNameHistory(member.id, member.guildId, nick, 'Register');
|
|
85
|
+
return true;
|
|
86
|
+
} catch (error) {
|
|
87
|
+
this.client.logger?.error(`Registration failed (User: ${member.id}):`, error);
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Record a name change in history.
|
|
94
|
+
* @param {string} userId
|
|
95
|
+
* @param {string} guildId
|
|
96
|
+
* @param {string} name
|
|
97
|
+
* @param {string} [reason='Name Change']
|
|
98
|
+
*/
|
|
99
|
+
async addNameHistory(userId, guildId, name, reason = 'Name Change') {
|
|
100
|
+
const key = this._nameHistoryKey(userId, guildId);
|
|
101
|
+
const raw = await this.client.cache.get(key);
|
|
102
|
+
const arr = this._parse(raw, []);
|
|
103
|
+
|
|
104
|
+
arr.push({ name, reason, date: Date.now() });
|
|
105
|
+
|
|
106
|
+
// Keep last 50 entries to prevent unbounded growth
|
|
107
|
+
if (arr.length > 50) arr.splice(0, arr.length - 50);
|
|
108
|
+
|
|
109
|
+
await this.client.cache.set(key, JSON.stringify(arr));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Get a user's name history (newest first).
|
|
114
|
+
* @param {string} userId
|
|
115
|
+
* @param {string} guildId
|
|
116
|
+
* @returns {Promise<Array<{ name: string, reason: string, date: number }>>}
|
|
117
|
+
*/
|
|
118
|
+
async getNameHistory(userId, guildId) {
|
|
119
|
+
const key = this._nameHistoryKey(userId, guildId);
|
|
120
|
+
const raw = await this.client.cache.get(key);
|
|
121
|
+
const arr = this._parse(raw, []);
|
|
122
|
+
return arr.sort((a, b) => b.date - a.date);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Jail a user by replacing all their roles with a jail role.
|
|
127
|
+
* Saves their current roles for restoration.
|
|
128
|
+
* @param {object} member
|
|
129
|
+
* @param {string} jailRoleId
|
|
130
|
+
* @returns {Promise<boolean>}
|
|
131
|
+
*/
|
|
132
|
+
async jailUser(member, jailRoleId) {
|
|
133
|
+
if (!member?.id || !member?.guildId) return false;
|
|
134
|
+
if (!jailRoleId) throw new TypeError('jailRoleId is required');
|
|
135
|
+
|
|
136
|
+
const currentRoles = Array.isArray(member.roles) ? member.roles : [];
|
|
137
|
+
const cacheKey = `jail_roles_${member.guildId}_${member.id}`;
|
|
138
|
+
|
|
139
|
+
await this.client.cache.set(cacheKey, JSON.stringify(currentRoles));
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
await this.client.rest.patch(`/guilds/${member.guildId}/members/${member.id}`, {
|
|
143
|
+
body: { roles: [jailRoleId] },
|
|
144
|
+
});
|
|
145
|
+
return true;
|
|
146
|
+
} catch (error) {
|
|
147
|
+
this.client.logger?.error('Jail failed:', error);
|
|
148
|
+
// Rollback cache write if API call failed
|
|
149
|
+
await this.client.cache.delete(cacheKey);
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Unjail a user by restoring their saved roles.
|
|
156
|
+
* @param {object} member
|
|
157
|
+
* @returns {Promise<boolean>}
|
|
158
|
+
*/
|
|
159
|
+
async unjailUser(member) {
|
|
160
|
+
if (!member?.id || !member?.guildId) return false;
|
|
161
|
+
|
|
162
|
+
const cacheKey = `jail_roles_${member.guildId}_${member.id}`;
|
|
163
|
+
const raw = await this.client.cache.get(cacheKey);
|
|
164
|
+
|
|
165
|
+
if (!raw) {
|
|
166
|
+
this.client.logger?.warn(`Unjail: No saved roles found for user ${member.id}.`);
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const rolesToRestore = this._parse(raw, []);
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
await this.client.rest.patch(`/guilds/${member.guildId}/members/${member.id}`, {
|
|
174
|
+
body: { roles: rolesToRestore },
|
|
175
|
+
});
|
|
176
|
+
await this.client.cache.delete(cacheKey);
|
|
177
|
+
return true;
|
|
178
|
+
} catch (error) {
|
|
179
|
+
this.client.logger?.error('Unjail failed:', error);
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Check if a user is currently jailed.
|
|
186
|
+
* @param {string} userId
|
|
187
|
+
* @param {string} guildId
|
|
188
|
+
* @returns {Promise<boolean>}
|
|
189
|
+
*/
|
|
190
|
+
async isJailed(userId, guildId) {
|
|
191
|
+
const cacheKey = `jail_roles_${guildId}_${userId}`;
|
|
192
|
+
const raw = await this.client.cache.get(cacheKey);
|
|
193
|
+
return raw !== undefined && raw !== null;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
module.exports = GuildManager;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
class InventoryManager {
|
|
4
|
+
/**
|
|
5
|
+
* @param {import('../client/Client')} client
|
|
6
|
+
*/
|
|
7
|
+
constructor(client) {
|
|
8
|
+
this.client = client;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
_key(userId) {
|
|
12
|
+
return `inv_items_${userId}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Parse the stored inventory safely.
|
|
17
|
+
* @private
|
|
18
|
+
*/
|
|
19
|
+
_parse(raw) {
|
|
20
|
+
if (!raw) return {};
|
|
21
|
+
if (typeof raw === 'object') return raw;
|
|
22
|
+
try {
|
|
23
|
+
return JSON.parse(raw);
|
|
24
|
+
} catch {
|
|
25
|
+
return {};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get a user's full inventory.
|
|
31
|
+
* @param {string} userId
|
|
32
|
+
* @returns {Promise<Record<string, number>>}
|
|
33
|
+
*/
|
|
34
|
+
async getInventory(userId) {
|
|
35
|
+
if (!userId) throw new TypeError('userId is required');
|
|
36
|
+
const val = await this.client.cache.get(this._key(userId));
|
|
37
|
+
return this._parse(val);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Add an item to a user's inventory.
|
|
42
|
+
* @param {string} userId
|
|
43
|
+
* @param {string} itemName
|
|
44
|
+
* @param {number} [amount=1]
|
|
45
|
+
* @returns {Promise<Record<string, number>>} Updated inventory
|
|
46
|
+
*/
|
|
47
|
+
async addItem(userId, itemName, amount = 1) {
|
|
48
|
+
if (!userId) throw new TypeError('userId is required');
|
|
49
|
+
if (!itemName || typeof itemName !== 'string') throw new TypeError('itemName must be a non-empty string');
|
|
50
|
+
if (typeof amount !== 'number' || amount <= 0 || !Number.isInteger(amount)) {
|
|
51
|
+
throw new RangeError('amount must be a positive integer');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const inv = await this.getInventory(userId);
|
|
55
|
+
inv[itemName] = (inv[itemName] ?? 0) + amount;
|
|
56
|
+
|
|
57
|
+
await this.client.cache.set(this._key(userId), JSON.stringify(inv));
|
|
58
|
+
return inv;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Remove an item from a user's inventory.
|
|
63
|
+
* @param {string} userId
|
|
64
|
+
* @param {string} itemName
|
|
65
|
+
* @param {number} [amount=1]
|
|
66
|
+
* @returns {Promise<Record<string, number>|false>} Updated inventory or false if not enough
|
|
67
|
+
*/
|
|
68
|
+
async removeItem(userId, itemName, amount = 1) {
|
|
69
|
+
if (!userId) throw new TypeError('userId is required');
|
|
70
|
+
if (!itemName || typeof itemName !== 'string') throw new TypeError('itemName must be a non-empty string');
|
|
71
|
+
if (typeof amount !== 'number' || amount <= 0 || !Number.isInteger(amount)) {
|
|
72
|
+
throw new RangeError('amount must be a positive integer');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const inv = await this.getInventory(userId);
|
|
76
|
+
const current = inv[itemName] ?? 0;
|
|
77
|
+
|
|
78
|
+
if (current < amount) return false;
|
|
79
|
+
|
|
80
|
+
inv[itemName] = current - amount;
|
|
81
|
+
if (inv[itemName] <= 0) {
|
|
82
|
+
delete inv[itemName];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
await this.client.cache.set(this._key(userId), JSON.stringify(inv));
|
|
86
|
+
return inv;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Check how many of an item a user has.
|
|
91
|
+
* @param {string} userId
|
|
92
|
+
* @param {string} itemName
|
|
93
|
+
* @returns {Promise<number>}
|
|
94
|
+
*/
|
|
95
|
+
async hasItem(userId, itemName) {
|
|
96
|
+
if (!userId) throw new TypeError('userId is required');
|
|
97
|
+
const inv = await this.getInventory(userId);
|
|
98
|
+
return inv[itemName] ?? 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Transfer an item from one user to another.
|
|
103
|
+
* @param {string} fromUserId
|
|
104
|
+
* @param {string} toUserId
|
|
105
|
+
* @param {string} itemName
|
|
106
|
+
* @param {number} [amount=1]
|
|
107
|
+
* @returns {Promise<boolean>}
|
|
108
|
+
*/
|
|
109
|
+
async transferItem(fromUserId, toUserId, itemName, amount = 1) {
|
|
110
|
+
if (!fromUserId || !toUserId) throw new TypeError('Both user IDs are required');
|
|
111
|
+
if (fromUserId === toUserId) throw new Error('Cannot transfer item to the same user');
|
|
112
|
+
|
|
113
|
+
const removed = await this.removeItem(fromUserId, itemName, amount);
|
|
114
|
+
if (removed === false) return false;
|
|
115
|
+
|
|
116
|
+
await this.addItem(toUserId, itemName, amount);
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Get all items in a user's inventory as an array.
|
|
122
|
+
* @param {string} userId
|
|
123
|
+
* @returns {Promise<Array<{ name: string, amount: number }>>}
|
|
124
|
+
*/
|
|
125
|
+
async getItemList(userId) {
|
|
126
|
+
const inv = await this.getInventory(userId);
|
|
127
|
+
return Object.entries(inv).map(([name, amount]) => ({ name, amount }));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Clear a user's entire inventory.
|
|
132
|
+
* @param {string} userId
|
|
133
|
+
*/
|
|
134
|
+
async clearInventory(userId) {
|
|
135
|
+
if (!userId) throw new TypeError('userId is required');
|
|
136
|
+
await this.client.cache.delete(this._key(userId));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
module.exports = InventoryManager;
|