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.
Files changed (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +274 -0
  3. package/SECURITY.md +160 -0
  4. package/index.js +90 -0
  5. package/package.json +68 -0
  6. package/src/cache/CacheManager.js +393 -0
  7. package/src/cache/MemoryAdapter.js +259 -0
  8. package/src/cache/MultiLevelCache.js +362 -0
  9. package/src/cache/RedisAdapter.js +329 -0
  10. package/src/cache/SQLiteAdapter.js +280 -0
  11. package/src/cache/index.js +15 -0
  12. package/src/client/Client.js +554 -0
  13. package/src/client/ShardingManager.js +274 -0
  14. package/src/config/ConfigValidator.js +172 -0
  15. package/src/config/index.js +7 -0
  16. package/src/dashboard/DashboardManager.js +362 -0
  17. package/src/datagen/DataGenerator.js +47 -0
  18. package/src/datagen/apis/GraphqlMocker.js +16 -0
  19. package/src/datagen/apis/OpenApiMocker.js +18 -0
  20. package/src/datagen/cli.js +130 -0
  21. package/src/datagen/formatters/csv.js +45 -0
  22. package/src/datagen/formatters/index.js +15 -0
  23. package/src/datagen/formatters/json.js +33 -0
  24. package/src/datagen/formatters/mongo.js +22 -0
  25. package/src/datagen/formatters/sql.js +32 -0
  26. package/src/datagen/index.js +17 -0
  27. package/src/datagen/plugin/PluginManager.js +43 -0
  28. package/src/datagen/plugins/example-plugin.js +21 -0
  29. package/src/datagen/schema/SchemaGenerator.js +172 -0
  30. package/src/datagen/schema/loadSchema.js +14 -0
  31. package/src/datagen/utils/seed.js +26 -0
  32. package/src/di/ServiceContainer.js +229 -0
  33. package/src/errors/ErrorCodes.js +110 -0
  34. package/src/errors/LumisError.js +85 -0
  35. package/src/game/EconomyManager.js +161 -0
  36. package/src/game/GameSessionManager.js +76 -0
  37. package/src/game/GuildManager.js +197 -0
  38. package/src/game/InventoryManager.js +140 -0
  39. package/src/game/LevelingSystem.js +194 -0
  40. package/src/game/MusicManager.js +587 -0
  41. package/src/health/HealthChecker.js +170 -0
  42. package/src/health/index.js +7 -0
  43. package/src/performance/PerformanceMonitor.js +244 -0
  44. package/src/performance/index.js +7 -0
  45. package/src/security/InputSanitizer.js +185 -0
  46. package/src/security/SecurityMiddleware.js +231 -0
  47. package/src/security/index.js +9 -0
  48. package/src/shutdown/GracefulShutdown.js +159 -0
  49. package/src/shutdown/index.js +7 -0
  50. package/src/utils/StructuredLogger.js +118 -0
@@ -0,0 +1,587 @@
1
+ 'use strict';
2
+
3
+ const { EventEmitter } = require('node:events');
4
+ const { request } = require('undici');
5
+ const Collection = require('../utils/Collection');
6
+ const { Events } = require('../utils/Constants');
7
+
8
+ const DEFAULT_OPTIONS = {
9
+ defaultSearchPlatform: 'ytsearch',
10
+ nodes: []
11
+ };
12
+
13
+ class LavalinkNode {
14
+ constructor({ host, port, password, secure = false, name } = {}) {
15
+ if (!host) throw new TypeError('lavalink node host is required');
16
+ if (!port) throw new TypeError('lavalink node port is required');
17
+ if (!password) throw new TypeError('lavalink node password is required');
18
+
19
+ this.host = host;
20
+ this.port = port;
21
+ this.password = password;
22
+ this.secure = Boolean(secure);
23
+ this.name = name || `${host}:${port}`;
24
+ }
25
+
26
+ get baseURL() {
27
+ return `${this.secure ? 'https' : 'http'}://${this.host}:${this.port}`;
28
+ }
29
+
30
+ async loadTracks(identifier) {
31
+ const url = `${this.baseURL}/loadtracks?identifier=${encodeURIComponent(identifier)}`;
32
+ const res = await request(url, {
33
+ headers: { Authorization: this.password }
34
+ });
35
+
36
+ if (res.statusCode !== 200) {
37
+ const body = await res.body.text();
38
+ throw new Error(`Lavalink loadtracks failed (${res.statusCode}): ${body}`);
39
+ }
40
+
41
+ return res.body.json();
42
+ }
43
+
44
+ async createSession(guildId, body) {
45
+ const url = `${this.baseURL}/v4/session/${guildId}`;
46
+ const res = await request(url, {
47
+ method: 'POST',
48
+ headers: {
49
+ Authorization: this.password,
50
+ 'Content-Type': 'application/json'
51
+ },
52
+ body: JSON.stringify(body)
53
+ });
54
+
55
+ if (res.statusCode !== 204) {
56
+ const text = await res.body.text();
57
+ throw new Error(`Lavalink session create failed (${res.statusCode}): ${text}`);
58
+ }
59
+
60
+ return true;
61
+ }
62
+
63
+ async play(guildId, track, options = {}) {
64
+ const url = `${this.baseURL}/v4/player/${guildId}/play`;
65
+
66
+ const body = {
67
+ track,
68
+ noReplace: options.noReplace ?? false,
69
+ pause: options.pause ?? false,
70
+ startTime: options.startTime ?? 0,
71
+ endTime: options.endTime ?? 0,
72
+ volume: options.volume ?? 100
73
+ };
74
+
75
+ const res = await request(url, {
76
+ method: 'POST',
77
+ headers: {
78
+ Authorization: this.password,
79
+ 'Content-Type': 'application/json'
80
+ },
81
+ body: JSON.stringify(body)
82
+ });
83
+
84
+ if (res.statusCode !== 204) {
85
+ const text = await res.body.text();
86
+ throw new Error(`Lavalink play failed (${res.statusCode}): ${text}`);
87
+ }
88
+
89
+ return true;
90
+ }
91
+
92
+ async stop(guildId) {
93
+ const url = `${this.baseURL}/v4/player/${guildId}/stop`;
94
+ const res = await request(url, {
95
+ method: 'POST',
96
+ headers: { Authorization: this.password }
97
+ });
98
+
99
+ if (res.statusCode !== 204) {
100
+ const text = await res.body.text();
101
+ throw new Error(`Lavalink stop failed (${res.statusCode}): ${text}`);
102
+ }
103
+
104
+ return true;
105
+ }
106
+ }
107
+
108
+ class MusicManager extends EventEmitter {
109
+ constructor(client, options = {}) {
110
+ super();
111
+
112
+ this.client = client;
113
+ this.options = { ...DEFAULT_OPTIONS, ...options };
114
+
115
+ /**
116
+ * Lavalink nodes (optional). Example:
117
+ * [{ host: '127.0.0.1', port: 2333, password: 'yours', secure: false }]
118
+ */
119
+ this.nodes = (this.options.nodes || []).map((node) => new LavalinkNode(node));
120
+ this._nodeIndex = 0;
121
+
122
+ /**
123
+ * Map of guild id -> player
124
+ * @type {Collection<string, MusicPlayer>}
125
+ */
126
+ this.players = new Collection();
127
+
128
+ /**
129
+ * Backward-compatible queue storage
130
+ * @type {Collection<string, Object>}
131
+ */
132
+ this.queues = new Collection();
133
+
134
+ /**
135
+ * Cache voice session data needed for Lavalink voice session updates.
136
+ * @type {Map<string, any>}
137
+ */
138
+ this.voiceStates = new Map();
139
+ this.voiceServers = new Map();
140
+
141
+ this.client.on(Events.VOICE_STATE_UPDATE, (data) => this.updateVoiceState(data));
142
+ this.client.on(Events.VOICE_SERVER_UPDATE, (data) => this.updateVoiceServer(data));
143
+ }
144
+
145
+ _getNextNode() {
146
+ if (!this.nodes.length) return null;
147
+ const node = this.nodes[this._nodeIndex % this.nodes.length];
148
+ this._nodeIndex += 1;
149
+ return node;
150
+ }
151
+
152
+ _formatSearchQuery(query) {
153
+ if (typeof query !== 'string') return query;
154
+ if (query.startsWith('http://') || query.startsWith('https://')) return query;
155
+ return `${this.options.defaultSearchPlatform}:${query}`;
156
+ }
157
+
158
+ /**
159
+ * Create or fetch an existing player for a guild.
160
+ * @param {Object} options
161
+ * @param {string} options.guildId
162
+ * @param {string} options.voiceChannel
163
+ * @param {string} [options.textChannel]
164
+ * @param {boolean} [options.deaf=true]
165
+ * @param {boolean} [options.mute=false]
166
+ */
167
+ createConnection({ guildId, voiceChannel, textChannel = null, deaf = true, mute = false } = {}) {
168
+ if (!guildId) throw new TypeError('guildId is required');
169
+ if (!voiceChannel) throw new TypeError('voiceChannel is required');
170
+
171
+ let player = this.players.get(guildId);
172
+ if (player) return player;
173
+
174
+ const node = this._getNextNode();
175
+
176
+ this.joinVoiceChannel(guildId, voiceChannel, { deaf, mute });
177
+
178
+ player = new MusicPlayer(this, {
179
+ guildId,
180
+ voiceChannel,
181
+ textChannel,
182
+ deaf,
183
+ mute,
184
+ node
185
+ });
186
+
187
+ this.players.set(guildId, player);
188
+ return player;
189
+ }
190
+
191
+ /**
192
+ * Resolve a search query into a track/playlist result.
193
+ * This is a minimal stub implementation. For real usage, replace with a proper search provider.
194
+ * @param {Object} options
195
+ * @param {string} options.query
196
+ * @param {any} [options.requester]
197
+ */
198
+ async resolve({ query, requester = null } = {}) {
199
+ if (!query) throw new TypeError('query is required');
200
+
201
+ // If nodes are configured, use Lavalink to resolve the query.
202
+ // Otherwise, fallback to a simple stub implementation.
203
+ const node = this._getNextNode();
204
+ if (node) {
205
+ const identifier = this._formatSearchQuery(query);
206
+ const data = await node.loadTracks(identifier);
207
+
208
+ const tracks = (data.tracks || []).map((t) => ({
209
+ title: t.info.title,
210
+ uri: t.info.uri,
211
+ identifier: t.info.identifier,
212
+ author: t.info.author,
213
+ duration: t.info.length,
214
+ isStream: t.info.isStream,
215
+ requester,
216
+ track: t.track,
217
+ raw: t
218
+ }));
219
+
220
+ return {
221
+ loadType: data.loadType,
222
+ tracks,
223
+ playlistInfo: data.playlistInfo || null
224
+ };
225
+ }
226
+
227
+ const track = this._buildTrack(query, requester);
228
+
229
+ return {
230
+ loadType: 'search',
231
+ tracks: [track],
232
+ playlistInfo: null
233
+ };
234
+ }
235
+
236
+ /**
237
+ * Build a track object similar to Aqualink's track shape.
238
+ * @param {string|Object} query
239
+ * @param {any} requester
240
+ */
241
+ _buildTrack(query, requester) {
242
+ const identifier = typeof query === 'string' ? query : query.uri || query.identifier;
243
+ const title = typeof query === 'string' ? query : query.title || identifier;
244
+
245
+ return {
246
+ title,
247
+ uri: identifier || '',
248
+ identifier: identifier || '',
249
+ author: (requester && requester.username) || 'Lumis',
250
+ duration: 0,
251
+ isStream: false,
252
+ requester
253
+ };
254
+ }
255
+
256
+ async joinVoiceChannel(guildId, channelId, options = {}) {
257
+ if (!this.client.ws || !this.client.ws.ws) {
258
+ this.client.logger.error('No WebSocket connection, cannot join voice channel.');
259
+ return false;
260
+ }
261
+
262
+ const { deaf = true, mute = false } = options;
263
+
264
+ const payload = {
265
+ op: 4,
266
+ d: {
267
+ guild_id: String(guildId),
268
+ channel_id: String(channelId),
269
+ self_mute: Boolean(mute),
270
+ self_deaf: Boolean(deaf)
271
+ }
272
+ };
273
+
274
+ this.client.ws.ws.send(JSON.stringify(payload));
275
+
276
+ if (!this.queues.has(guildId)) {
277
+ this.queues.set(guildId, {
278
+ textChannel: null,
279
+ voiceChannel: channelId,
280
+ connection: null, // İleride WebRTC bağlantısı eklenebilir
281
+ songs: [],
282
+ volume: 100,
283
+ playing: false
284
+ });
285
+ } else {
286
+ const queue = this.queues.get(guildId);
287
+ queue.voiceChannel = channelId;
288
+ }
289
+
290
+ this.client.logger.info(`Voice channel join request sent: ${channelId}`);
291
+ return true;
292
+ }
293
+
294
+ async leaveVoiceChannel(guildId) {
295
+ if (!this.client.ws || !this.client.ws.ws) return;
296
+
297
+ const payload = {
298
+ op: 4,
299
+ d: {
300
+ guild_id: String(guildId),
301
+ channel_id: null,
302
+ self_mute: false,
303
+ self_deaf: false
304
+ }
305
+ };
306
+
307
+ this.client.ws.ws.send(JSON.stringify(payload));
308
+
309
+ this.queues.delete(guildId);
310
+ this.voiceStates.delete(guildId);
311
+ this.voiceServers.delete(guildId);
312
+
313
+ const player = this.players.get(guildId);
314
+ if (player) player.destroy();
315
+ this.players.delete(guildId);
316
+ }
317
+
318
+ async updateVoiceState(data) {
319
+ const { guild_id: guildId } = data;
320
+ this.voiceStates.set(guildId, data);
321
+
322
+ const player = this.players.get(guildId);
323
+ if (!player) return;
324
+
325
+ player.voiceState = data;
326
+ await this._sendLavalinkVoiceUpdate(player);
327
+ }
328
+
329
+ async updateVoiceServer(data) {
330
+ const { guild_id: guildId } = data;
331
+ this.voiceServers.set(guildId, data);
332
+
333
+ const player = this.players.get(guildId);
334
+ if (!player) return;
335
+
336
+ player.voiceServer = data;
337
+ await this._sendLavalinkVoiceUpdate(player);
338
+ }
339
+
340
+ async _sendLavalinkVoiceUpdate(player) {
341
+ if (!player.voiceServer || !player.voiceState || !player.node) return;
342
+
343
+ try {
344
+ await player.node.createSession(player.guildId, {
345
+ voiceServer: player.voiceServer,
346
+ voiceState: player.voiceState
347
+ });
348
+ } catch (error) {
349
+ this.client.logger.error(`Lavalink voice update failed for guild ${player.guildId}: ${error.message}`);
350
+ }
351
+ }
352
+
353
+ /**
354
+ * Backwards compatible helper.
355
+ * @param {string} guildId
356
+ */
357
+ getQueue(guildId) {
358
+ const player = this.players.get(guildId);
359
+ if (player) return player.queue;
360
+ return this.queues.get(guildId) || null;
361
+ }
362
+
363
+ /**
364
+ * Backwards-compatible helper to add a track to a guild queue.
365
+ * @param {string} guildId
366
+ * @param {Object} song
367
+ */
368
+ addTrack(guildId, song) {
369
+ const player = this.players.get(guildId);
370
+ if (player) {
371
+ player.queue.add(song);
372
+ return true;
373
+ }
374
+
375
+ const queue = this.queues.get(guildId);
376
+ if (!queue) {
377
+ this.client.logger.warn(`Queue not found. Please join a voice channel first (${guildId}).`);
378
+ return false;
379
+ }
380
+
381
+ queue.songs.push(song);
382
+ return true;
383
+ }
384
+
385
+ /**
386
+ * Backwards-compatible play helper.
387
+ * @param {string} guildId
388
+ */
389
+ play(guildId) {
390
+ const player = this.players.get(guildId);
391
+ if (player) return player.play();
392
+
393
+ const queue = this.queues.get(guildId);
394
+ if (!queue || queue.songs.length === 0) return false;
395
+
396
+ queue.playing = true;
397
+ const currentSong = queue.songs[0];
398
+
399
+ this.client.logger.info(`(SIMULATED) Now playing: ${currentSong.title} (Duration: ${currentSong.duration})`);
400
+ return true;
401
+ }
402
+
403
+ /**
404
+ * Backwards-compatible skip helper.
405
+ * @param {string} guildId
406
+ */
407
+ skip(guildId) {
408
+ const player = this.players.get(guildId);
409
+ if (player) return player.skip();
410
+
411
+ const queue = this.queues.get(guildId);
412
+ if (!queue || queue.songs.length === 0) return false;
413
+
414
+ const skipped = queue.songs.shift();
415
+
416
+ if (queue.songs.length > 0) {
417
+ this.play(guildId);
418
+ } else {
419
+ queue.playing = false;
420
+ }
421
+
422
+ return skipped;
423
+ }
424
+ }
425
+
426
+ class PlayerQueue {
427
+ constructor(player) {
428
+ this.player = player;
429
+ this.tracks = [];
430
+ }
431
+
432
+ add(...tracks) {
433
+ this.tracks.push(...tracks);
434
+ return this.tracks.length;
435
+ }
436
+
437
+ shift() {
438
+ return this.tracks.shift();
439
+ }
440
+
441
+ remove(index) {
442
+ if (index < 0 || index >= this.tracks.length) return null;
443
+ return this.tracks.splice(index, 1)[0];
444
+ }
445
+
446
+ clear() {
447
+ this.tracks = [];
448
+ return this.tracks;
449
+ }
450
+
451
+ shuffle() {
452
+ for (let i = this.tracks.length - 1; i > 0; i -= 1) {
453
+ const j = Math.floor(Math.random() * (i + 1));
454
+ [this.tracks[i], this.tracks[j]] = [this.tracks[j], this.tracks[i]];
455
+ }
456
+ return this.tracks;
457
+ }
458
+
459
+ get size() {
460
+ return this.tracks.length;
461
+ }
462
+
463
+ get first() {
464
+ return this.tracks[0] || null;
465
+ }
466
+
467
+ toArray() {
468
+ return [...this.tracks];
469
+ }
470
+ }
471
+
472
+ class MusicPlayer {
473
+ constructor(manager, options = {}) {
474
+ this.manager = manager;
475
+ this.guildId = options.guildId;
476
+ this.voiceChannel = options.voiceChannel;
477
+ this.textChannel = options.textChannel || null;
478
+
479
+ this.deaf = Boolean(options.deaf);
480
+ this.mute = Boolean(options.mute);
481
+
482
+ this.queue = new PlayerQueue(this);
483
+ this.playing = false;
484
+ this.paused = false;
485
+ this.volume = 100;
486
+ this.current = null;
487
+ }
488
+
489
+ async play() {
490
+ if (!this.queue.size) return false;
491
+
492
+ const next = this.queue.first;
493
+ if (!next) return false;
494
+
495
+ this.current = next;
496
+ this.playing = true;
497
+ this.paused = false;
498
+
499
+ if (this.node && next.track) {
500
+ try {
501
+ await this.node.play(this.guildId, next.track, { volume: this.volume });
502
+ } catch (error) {
503
+ this.manager.client.logger.error(`Lavalink play failed: ${error.message}`);
504
+ }
505
+ }
506
+
507
+ this.manager.client.logger.info(`Now playing: ${next.title}`);
508
+ this.manager.emit('trackStart', this, next);
509
+
510
+ return true;
511
+ }
512
+
513
+ async pause() {
514
+ if (!this.playing || this.paused) return false;
515
+ this.paused = true;
516
+ this.manager.emit('trackPause', this, this.current);
517
+ return true;
518
+ }
519
+
520
+ async resume() {
521
+ if (!this.playing || !this.paused) return false;
522
+ this.paused = false;
523
+ this.manager.emit('trackResume', this, this.current);
524
+ return true;
525
+ }
526
+
527
+ async skip() {
528
+ if (!this.playing) return false;
529
+
530
+ const skipped = this.queue.shift();
531
+ this.manager.emit('trackSkip', this, skipped);
532
+
533
+ if (this.node) {
534
+ try {
535
+ await this.node.stop(this.guildId);
536
+ } catch (error) {
537
+ this.manager.client.logger.error(`Lavalink stop failed: ${error.message}`);
538
+ }
539
+ }
540
+
541
+ if (this.queue.size > 0) {
542
+ await this.play();
543
+ return skipped;
544
+ }
545
+
546
+ this.playing = false;
547
+ this.current = null;
548
+ this.manager.emit('queueEnd', this);
549
+ return skipped;
550
+ }
551
+
552
+ async stop() {
553
+ this.playing = false;
554
+ this.paused = false;
555
+ this.current = null;
556
+ this.queue.clear();
557
+ this.manager.emit('stop', this);
558
+ return true;
559
+ }
560
+
561
+ setVolume(volume) {
562
+ this.volume = Number(volume);
563
+ this.manager.emit('volumeChange', this, this.volume);
564
+ return this.volume;
565
+ }
566
+
567
+ async destroy() {
568
+ await this.stop();
569
+ this.manager.players.delete(this.guildId);
570
+ this.manager.queues.delete(this.guildId);
571
+ this.manager.emit('destroy', this);
572
+ return true;
573
+ }
574
+ }
575
+
576
+ MusicManager.Events = {
577
+ TrackStart: 'trackStart',
578
+ TrackPause: 'trackPause',
579
+ TrackResume: 'trackResume',
580
+ TrackSkip: 'trackSkip',
581
+ QueueEnd: 'queueEnd',
582
+ Stop: 'stop',
583
+ Destroy: 'destroy',
584
+ VolumeChange: 'volumeChange'
585
+ };
586
+
587
+ module.exports = MusicManager;