@tryty/easydc-js 1.1.0 → 1.2.2
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 +192 -17
- package/examples/basic.js +1 -1
- package/examples/mongo.js +44 -0
- package/index.d.ts +29 -6
- package/mongodb.d.ts +31 -0
- package/package.json +37 -4
- package/src/bot.js +117 -14
- package/src/database.js +24 -1
- package/src/functions/control.js +2 -1
- package/src/functions/eval.js +2 -1
- package/src/functions/messages.js +6 -5
- package/src/functions/vars.js +8 -4
- package/src/index.js +2 -1
- package/src/owners.js +36 -0
- package/src/plugins/mongodb.js +140 -0
- package/src/plugins/voice.js +1030 -0
- package/src/selftest.js +215 -0
- package/voice.d.ts +29 -0
|
@@ -0,0 +1,1030 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { BotError } = require('../errors');
|
|
4
|
+
|
|
5
|
+
const has = (v) => v !== undefined && v !== null && String(v).trim() !== '';
|
|
6
|
+
|
|
7
|
+
function truthy(v) {
|
|
8
|
+
if (typeof v === 'boolean') return v;
|
|
9
|
+
const s = String(v ?? '').trim().toLowerCase();
|
|
10
|
+
return ['true', 'yes', '1', 'on', 'ya'].includes(s);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function formatDuration(ms) {
|
|
14
|
+
if (!ms || !Number.isFinite(ms) || ms <= 0) return '00:00';
|
|
15
|
+
const totalSeconds = Math.floor(ms / 1000);
|
|
16
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
17
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
18
|
+
const seconds = totalSeconds % 60;
|
|
19
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
20
|
+
if (hours > 0) return `${hours}:${pad(minutes)}:${pad(seconds)}`;
|
|
21
|
+
return `${pad(minutes)}:${pad(seconds)}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function parseDurationMs(str) {
|
|
25
|
+
if (!str) return 0;
|
|
26
|
+
const s = String(str).trim();
|
|
27
|
+
if (s.includes(':')) {
|
|
28
|
+
const parts = s.split(':').map((p) => parseInt(p, 10) || 0);
|
|
29
|
+
if (parts.length === 2) return (parts[0] * 60 + parts[1]) * 1000;
|
|
30
|
+
if (parts.length === 3) return (parts[0] * 3600 + parts[1] * 60 + parts[2]) * 1000;
|
|
31
|
+
}
|
|
32
|
+
const sec = parseFloat(s.replace(/s$/i, ''));
|
|
33
|
+
if (Number.isFinite(sec)) return Math.floor(sec * 1000);
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function formatTrack(raw, requester = '') {
|
|
38
|
+
if (!raw) return null;
|
|
39
|
+
const info = raw.info ?? {};
|
|
40
|
+
return {
|
|
41
|
+
encoded: raw.encoded ?? raw.track ?? '',
|
|
42
|
+
title: info.title ?? 'Tidak diketahui',
|
|
43
|
+
author: info.author ?? 'Tidak diketahui',
|
|
44
|
+
length: Number(info.length ?? info.duration ?? 0),
|
|
45
|
+
identifier: info.identifier ?? '',
|
|
46
|
+
isSeekable: Boolean(info.isSeekable),
|
|
47
|
+
isStream: Boolean(info.isStream),
|
|
48
|
+
uri: info.uri ?? '',
|
|
49
|
+
artworkUrl: info.artworkUrl ?? null,
|
|
50
|
+
requester,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function extractResult(res, requester = '') {
|
|
55
|
+
if (!res) return { type: 'empty', tracks: [] };
|
|
56
|
+
// Lavalink v4
|
|
57
|
+
if (res.loadType === 'track') {
|
|
58
|
+
return { type: 'track', tracks: [formatTrack(res.data, requester)].filter(Boolean) };
|
|
59
|
+
}
|
|
60
|
+
if (res.loadType === 'playlist') {
|
|
61
|
+
const list = (res.data?.tracks ?? []).map((t) => formatTrack(t, requester)).filter(Boolean);
|
|
62
|
+
return { type: 'playlist', name: res.data?.info?.name ?? 'Playlist', tracks: list };
|
|
63
|
+
}
|
|
64
|
+
if (res.loadType === 'search') {
|
|
65
|
+
const list = (Array.isArray(res.data) ? res.data : []).map((t) => formatTrack(t, requester)).filter(Boolean);
|
|
66
|
+
return { type: 'search', tracks: list };
|
|
67
|
+
}
|
|
68
|
+
if (res.loadType === 'empty') {
|
|
69
|
+
return { type: 'empty', tracks: [] };
|
|
70
|
+
}
|
|
71
|
+
// Lavalink v3
|
|
72
|
+
if (res.loadType === 'TRACK_LOADED') {
|
|
73
|
+
return { type: 'track', tracks: [formatTrack(res.tracks?.[0], requester)].filter(Boolean) };
|
|
74
|
+
}
|
|
75
|
+
if (res.loadType === 'PLAYLIST_LOADED') {
|
|
76
|
+
const list = (res.tracks ?? []).map((t) => formatTrack(t, requester)).filter(Boolean);
|
|
77
|
+
return { type: 'playlist', name: res.playlistInfo?.name ?? 'Playlist', tracks: list };
|
|
78
|
+
}
|
|
79
|
+
if (res.loadType === 'SEARCH_RESULT') {
|
|
80
|
+
const list = (res.tracks ?? []).map((t) => formatTrack(t, requester)).filter(Boolean);
|
|
81
|
+
return { type: 'search', tracks: list };
|
|
82
|
+
}
|
|
83
|
+
return { type: 'empty', tracks: [] };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Antrean musik per server (guild)
|
|
88
|
+
*/
|
|
89
|
+
class GuildQueue {
|
|
90
|
+
constructor(guildId, manager) {
|
|
91
|
+
this.guildId = guildId;
|
|
92
|
+
this.manager = manager;
|
|
93
|
+
this.player = null;
|
|
94
|
+
this.tracks = [];
|
|
95
|
+
this.current = null;
|
|
96
|
+
this.loop = 'off'; // 'off' | 'track' | 'queue'
|
|
97
|
+
this.volume = 100;
|
|
98
|
+
this.paused = false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
get isPlaying() {
|
|
102
|
+
return Boolean(this.current);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async play() {
|
|
106
|
+
if (!this.player) return;
|
|
107
|
+
if (!this.current) {
|
|
108
|
+
this.current = this.tracks.shift() ?? null;
|
|
109
|
+
}
|
|
110
|
+
if (!this.current) return;
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
const trackData = this.current.encoded;
|
|
114
|
+
await this.player.playTrack({ track: { encoded: trackData } });
|
|
115
|
+
this.paused = false;
|
|
116
|
+
} catch (e) {
|
|
117
|
+
console.error(`[easydc-voice] Gagal memutar lagu: ${e.message}`);
|
|
118
|
+
this.current = null;
|
|
119
|
+
if (this.tracks.length > 0) {
|
|
120
|
+
await this.play();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async skip() {
|
|
126
|
+
if (!this.player || !this.current) return null;
|
|
127
|
+
const skipped = this.current;
|
|
128
|
+
if (this.loop === 'track') this.loop = 'off';
|
|
129
|
+
await this.player.stopTrack();
|
|
130
|
+
return skipped;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async stop() {
|
|
134
|
+
this.tracks = [];
|
|
135
|
+
this.current = null;
|
|
136
|
+
this.loop = 'off';
|
|
137
|
+
this.paused = false;
|
|
138
|
+
if (this.player) {
|
|
139
|
+
try {
|
|
140
|
+
await this.player.stopTrack();
|
|
141
|
+
} catch { /* abaikan jika sudah berhenti */ }
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
destroy() {
|
|
146
|
+
this.tracks = [];
|
|
147
|
+
this.current = null;
|
|
148
|
+
this.player = null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Voice & Music Manager
|
|
154
|
+
*/
|
|
155
|
+
class VoiceManager {
|
|
156
|
+
constructor(bot, options = {}) {
|
|
157
|
+
this.bot = bot;
|
|
158
|
+
this.options = {
|
|
159
|
+
nodes: options.nodes ?? [],
|
|
160
|
+
defaultSearchEngine: options.defaultSearchEngine ?? 'ytsearch',
|
|
161
|
+
shoukakuOptions: options.shoukakuOptions ?? {},
|
|
162
|
+
driver: options.driver ?? null,
|
|
163
|
+
...options,
|
|
164
|
+
};
|
|
165
|
+
this.shoukaku = null;
|
|
166
|
+
this.queues = new Map();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
_loadDriver() {
|
|
170
|
+
if (this.options.driver) return this.options.driver;
|
|
171
|
+
try {
|
|
172
|
+
// eslint-disable-next-line global-require
|
|
173
|
+
return require('shoukaku');
|
|
174
|
+
} catch (e) {
|
|
175
|
+
throw new BotError('Package "shoukaku" tidak ditemukan. Jalankan: npm install shoukaku', { cause: e });
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
initShoukaku(client) {
|
|
180
|
+
if (this.shoukaku || !this.options.nodes.length) return;
|
|
181
|
+
const { Shoukaku, Connectors } = this._loadDriver();
|
|
182
|
+
this.shoukaku = new Shoukaku(
|
|
183
|
+
new Connectors.DiscordJS(client),
|
|
184
|
+
this.options.nodes,
|
|
185
|
+
this.options.shoukakuOptions,
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
this.shoukaku.on('ready', (name) => {
|
|
189
|
+
console.log(`[easydc-voice] Node Lavalink "${name}" terhubung dan siap.`);
|
|
190
|
+
});
|
|
191
|
+
this.shoukaku.on('error', (name, error) => {
|
|
192
|
+
console.error(`[easydc-voice] Node Lavalink "${name}" error: ${error?.message ?? error}`);
|
|
193
|
+
});
|
|
194
|
+
this.shoukaku.on('close', (name, code, reason) => {
|
|
195
|
+
console.warn(`[easydc-voice] Node Lavalink "${name}" terputus (${code}): ${reason}`);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
client.on('voiceStateUpdate', (oldS, newS) => {
|
|
199
|
+
if (newS.member?.id === client.user?.id && !newS.channelId) {
|
|
200
|
+
const q = this.queues.get(newS.guild.id);
|
|
201
|
+
if (q) {
|
|
202
|
+
q.destroy();
|
|
203
|
+
this.queues.delete(newS.guild.id);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
getQueue(guildId) {
|
|
210
|
+
let queue = this.queues.get(guildId);
|
|
211
|
+
if (!queue) {
|
|
212
|
+
queue = new GuildQueue(guildId, this);
|
|
213
|
+
this.queues.set(guildId, queue);
|
|
214
|
+
}
|
|
215
|
+
return queue;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
bindPlayerEvents(queue, player) {
|
|
219
|
+
player.on('end', async (data) => {
|
|
220
|
+
const reason = data?.reason ?? data;
|
|
221
|
+
if (reason === 'replaced') return;
|
|
222
|
+
|
|
223
|
+
if (queue.loop === 'track' && queue.current && reason === 'finished') {
|
|
224
|
+
await queue.play();
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
if (queue.loop === 'queue' && queue.current && reason === 'finished') {
|
|
228
|
+
queue.tracks.push(queue.current);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
queue.current = null;
|
|
232
|
+
if (queue.tracks.length > 0) {
|
|
233
|
+
await queue.play();
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
player.on('exception', (err) => {
|
|
238
|
+
console.error(`[easydc-voice] Exception pada player: ${err?.message ?? err}`);
|
|
239
|
+
queue.current = null;
|
|
240
|
+
if (queue.tracks.length > 0) queue.play().catch(() => {});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
player.on('closed', () => {
|
|
244
|
+
queue.destroy();
|
|
245
|
+
this.queues.delete(queue.guildId);
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async joinVoice(guild, channelId) {
|
|
250
|
+
if (this.shoukaku && this.options.nodes.length) {
|
|
251
|
+
const player = await this.shoukaku.joinVoiceChannel({
|
|
252
|
+
guildId: guild.id,
|
|
253
|
+
channelId,
|
|
254
|
+
shardId: guild.shardId ?? guild.shard?.id ?? 0,
|
|
255
|
+
deaf: true,
|
|
256
|
+
});
|
|
257
|
+
const queue = this.getQueue(guild.id);
|
|
258
|
+
queue.player = player;
|
|
259
|
+
this.bindPlayerEvents(queue, player);
|
|
260
|
+
return player;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Opsi C: Tanpa Lavalink, kirim Opcode 4 langsung
|
|
264
|
+
if (guild.shard) {
|
|
265
|
+
guild.shard.send({
|
|
266
|
+
op: 4,
|
|
267
|
+
d: {
|
|
268
|
+
guild_id: guild.id,
|
|
269
|
+
channel_id: channelId,
|
|
270
|
+
self_mute: false,
|
|
271
|
+
self_deaf: true,
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async leaveVoice(guild) {
|
|
279
|
+
const queue = this.queues.get(guild.id);
|
|
280
|
+
if (queue) {
|
|
281
|
+
await queue.stop();
|
|
282
|
+
queue.destroy();
|
|
283
|
+
this.queues.delete(guild.id);
|
|
284
|
+
}
|
|
285
|
+
if (this.shoukaku && this.options.nodes.length) {
|
|
286
|
+
try {
|
|
287
|
+
await this.shoukaku.leaveVoiceChannel(guild.id);
|
|
288
|
+
} catch { /* abaikan */ }
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if (guild.shard) {
|
|
292
|
+
guild.shard.send({
|
|
293
|
+
op: 4,
|
|
294
|
+
d: {
|
|
295
|
+
guild_id: guild.id,
|
|
296
|
+
channel_id: null,
|
|
297
|
+
self_mute: false,
|
|
298
|
+
self_deaf: false,
|
|
299
|
+
},
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
hookBot() {
|
|
305
|
+
const self = this;
|
|
306
|
+
|
|
307
|
+
const origBind = this.bot._bind;
|
|
308
|
+
this.bot._bind = function (client, discord) {
|
|
309
|
+
origBind.call(this, client, discord);
|
|
310
|
+
if (self.options.nodes.length > 0) {
|
|
311
|
+
self.initShoukaku(client);
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const origDestroy = this.bot.destroy;
|
|
316
|
+
this.bot.destroy = async function () {
|
|
317
|
+
await self.destroy();
|
|
318
|
+
return origDestroy.call(this);
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async destroy() {
|
|
323
|
+
for (const queue of this.queues.values()) {
|
|
324
|
+
queue.destroy();
|
|
325
|
+
}
|
|
326
|
+
this.queues.clear();
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
installFunctions() {
|
|
330
|
+
const bot = this.bot;
|
|
331
|
+
const manager = this;
|
|
332
|
+
|
|
333
|
+
const resolveGuild = (ctx, guildId) => {
|
|
334
|
+
if (has(guildId)) {
|
|
335
|
+
const g = bot.client?.guilds?.cache?.get(String(guildId).trim());
|
|
336
|
+
if (g) return g;
|
|
337
|
+
}
|
|
338
|
+
if (ctx.guild) return ctx.guild;
|
|
339
|
+
throw new BotError('Perintah ini hanya bisa dipakai di dalam server');
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
const register = (spec) => {
|
|
343
|
+
bot.addFunction(spec);
|
|
344
|
+
const raw = String(spec.name).replace(/^\$/, '').toLowerCase();
|
|
345
|
+
const def = bot.registry.get(raw);
|
|
346
|
+
if (def) def.category = 'Voice';
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
// =========================================================================
|
|
350
|
+
// 1. KONEKSI VOICE DASAR (Opsi C)
|
|
351
|
+
// =========================================================================
|
|
352
|
+
|
|
353
|
+
register({
|
|
354
|
+
name: '$joinVC',
|
|
355
|
+
brackets: 'optional',
|
|
356
|
+
min: 0,
|
|
357
|
+
max: 1,
|
|
358
|
+
signature: '[channelID?]',
|
|
359
|
+
description: 'Bergabung ke voice channel (default: channel pengirim)',
|
|
360
|
+
run: async (ctx, [channelId]) => {
|
|
361
|
+
const guild = resolveGuild(ctx);
|
|
362
|
+
const targetId = has(channelId) ? String(channelId).trim() : ctx.member?.voice?.channelId;
|
|
363
|
+
if (!targetId) {
|
|
364
|
+
throw new BotError('Kamu harus berada di voice channel atau masukkan ID voice channel tujuan');
|
|
365
|
+
}
|
|
366
|
+
await manager.joinVoice(guild, targetId);
|
|
367
|
+
return '';
|
|
368
|
+
},
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
register({
|
|
372
|
+
name: '$leaveVC',
|
|
373
|
+
brackets: 'optional',
|
|
374
|
+
min: 0,
|
|
375
|
+
max: 1,
|
|
376
|
+
signature: '[guildID?]',
|
|
377
|
+
description: 'Keluar dari voice channel',
|
|
378
|
+
run: async (ctx, [guildId]) => {
|
|
379
|
+
const guild = resolveGuild(ctx, guildId);
|
|
380
|
+
await manager.leaveVoice(guild);
|
|
381
|
+
return '';
|
|
382
|
+
},
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
register({
|
|
386
|
+
name: '$isVoiceConnected',
|
|
387
|
+
brackets: 'optional',
|
|
388
|
+
min: 0,
|
|
389
|
+
max: 1,
|
|
390
|
+
signature: '[guildID?]',
|
|
391
|
+
description: 'true/false: apakah bot sedang terhubung ke voice channel',
|
|
392
|
+
run: (ctx, [guildId]) => {
|
|
393
|
+
const guild = resolveGuild(ctx, guildId);
|
|
394
|
+
return String(Boolean(guild.members?.me?.voice?.channelId));
|
|
395
|
+
},
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
register({
|
|
399
|
+
name: '$voiceChannelID',
|
|
400
|
+
brackets: 'optional',
|
|
401
|
+
min: 0,
|
|
402
|
+
max: 1,
|
|
403
|
+
signature: '[guildID?]',
|
|
404
|
+
description: 'ID voice channel tempat bot berada saat ini',
|
|
405
|
+
run: (ctx, [guildId]) => {
|
|
406
|
+
const guild = resolveGuild(ctx, guildId);
|
|
407
|
+
return guild.members?.me?.voice?.channelId ?? '';
|
|
408
|
+
},
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
register({
|
|
412
|
+
name: '$voiceDeaf',
|
|
413
|
+
brackets: 'optional',
|
|
414
|
+
min: 0,
|
|
415
|
+
max: 2,
|
|
416
|
+
signature: '[deaf?;guildID?]',
|
|
417
|
+
description: 'Atur (true/false) atau baca status deafen bot di voice channel',
|
|
418
|
+
run: async (ctx, [deaf, guildId]) => {
|
|
419
|
+
const guild = resolveGuild(ctx, guildId);
|
|
420
|
+
const me = guild.members?.me;
|
|
421
|
+
if (!me?.voice?.channelId) throw new BotError('Bot tidak sedang berada di voice channel');
|
|
422
|
+
if (has(deaf)) {
|
|
423
|
+
const val = truthy(deaf);
|
|
424
|
+
await me.voice.setDeaf(val);
|
|
425
|
+
return String(val);
|
|
426
|
+
}
|
|
427
|
+
return String(Boolean(me.voice.deaf));
|
|
428
|
+
},
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
register({
|
|
432
|
+
name: '$voiceMute',
|
|
433
|
+
brackets: 'optional',
|
|
434
|
+
min: 0,
|
|
435
|
+
max: 2,
|
|
436
|
+
signature: '[mute?;guildID?]',
|
|
437
|
+
description: 'Atur (true/false) atau baca status mute bot di voice channel',
|
|
438
|
+
run: async (ctx, [mute, guildId]) => {
|
|
439
|
+
const guild = resolveGuild(ctx, guildId);
|
|
440
|
+
const me = guild.members?.me;
|
|
441
|
+
if (!me?.voice?.channelId) throw new BotError('Bot tidak sedang berada di voice channel');
|
|
442
|
+
if (has(mute)) {
|
|
443
|
+
const val = truthy(mute);
|
|
444
|
+
await me.voice.setMute(val);
|
|
445
|
+
return String(val);
|
|
446
|
+
}
|
|
447
|
+
return String(Boolean(me.voice.mute));
|
|
448
|
+
},
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
// =========================================================================
|
|
452
|
+
// 2. PEMUTARAN & KONTROL MUSIK (Opsi B)
|
|
453
|
+
// =========================================================================
|
|
454
|
+
|
|
455
|
+
register({
|
|
456
|
+
name: '$play',
|
|
457
|
+
brackets: 'required',
|
|
458
|
+
min: 1,
|
|
459
|
+
max: 2,
|
|
460
|
+
signature: '[query;source?]',
|
|
461
|
+
description: 'Putar lagu atau tambah ke antrean lewat Lavalink',
|
|
462
|
+
run: async (ctx, [query, source]) => {
|
|
463
|
+
const guild = resolveGuild(ctx);
|
|
464
|
+
if (!manager.shoukaku || !manager.options.nodes.length) {
|
|
465
|
+
throw new BotError('Fitur musik membutuhkan server Lavalink. Konfigurasikan opsi "nodes" di voice({ nodes: [...] })');
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const botVC = guild.members?.me?.voice?.channelId;
|
|
469
|
+
const userVC = ctx.member?.voice?.channelId;
|
|
470
|
+
if (!botVC && !userVC) {
|
|
471
|
+
throw new BotError('Kamu harus berada di voice channel terlebih dahulu');
|
|
472
|
+
}
|
|
473
|
+
const targetVC = botVC || userVC;
|
|
474
|
+
|
|
475
|
+
const queue = manager.getQueue(guild.id);
|
|
476
|
+
if (!queue.player || !botVC) {
|
|
477
|
+
await manager.joinVoice(guild, targetVC);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const node = manager.shoukaku.getIdealNode();
|
|
481
|
+
if (!node) throw new BotError('Tidak ada server Lavalink yang terhubung');
|
|
482
|
+
|
|
483
|
+
const requester = ctx.user?.username ?? ctx.user?.tag ?? 'Seseorang';
|
|
484
|
+
const isUrl = /^https?:\/\//i.test(query.trim());
|
|
485
|
+
const searchEngine = has(source) ? source.trim() : manager.options.defaultSearchEngine;
|
|
486
|
+
const identifier = isUrl ? query.trim() : `${searchEngine}:${query.trim()}`;
|
|
487
|
+
|
|
488
|
+
const res = await node.rest.resolve(identifier);
|
|
489
|
+
const result = extractResult(res, requester);
|
|
490
|
+
|
|
491
|
+
if (!result.tracks.length) {
|
|
492
|
+
throw new BotError(`Tidak ada lagu yang ditemukan untuk "${query.trim()}"`);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (result.type === 'playlist') {
|
|
496
|
+
queue.tracks.push(...result.tracks);
|
|
497
|
+
if (!queue.isPlaying) await queue.play();
|
|
498
|
+
return `${result.tracks.length} lagu dari ${result.name}`;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const track = result.tracks[0];
|
|
502
|
+
queue.tracks.push(track);
|
|
503
|
+
if (!queue.isPlaying) {
|
|
504
|
+
await queue.play();
|
|
505
|
+
}
|
|
506
|
+
return track.title;
|
|
507
|
+
},
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
register({
|
|
511
|
+
name: '$pause',
|
|
512
|
+
brackets: 'optional',
|
|
513
|
+
min: 0,
|
|
514
|
+
max: 1,
|
|
515
|
+
signature: '[guildID?]',
|
|
516
|
+
description: 'Jeda pemutaran musik',
|
|
517
|
+
run: async (ctx, [guildId]) => {
|
|
518
|
+
const guild = resolveGuild(ctx, guildId);
|
|
519
|
+
const queue = manager.queues.get(guild.id);
|
|
520
|
+
if (!queue?.player || !queue.isPlaying) return '';
|
|
521
|
+
if (!queue.paused) {
|
|
522
|
+
await queue.player.setPaused(true);
|
|
523
|
+
queue.paused = true;
|
|
524
|
+
}
|
|
525
|
+
return 'true';
|
|
526
|
+
},
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
register({
|
|
530
|
+
name: '$resume',
|
|
531
|
+
brackets: 'optional',
|
|
532
|
+
min: 0,
|
|
533
|
+
max: 1,
|
|
534
|
+
signature: '[guildID?]',
|
|
535
|
+
description: 'Lanjutkan pemutaran musik yang dijeda',
|
|
536
|
+
run: async (ctx, [guildId]) => {
|
|
537
|
+
const guild = resolveGuild(ctx, guildId);
|
|
538
|
+
const queue = manager.queues.get(guild.id);
|
|
539
|
+
if (!queue?.player || !queue.isPlaying) return '';
|
|
540
|
+
if (queue.paused) {
|
|
541
|
+
await queue.player.setPaused(false);
|
|
542
|
+
queue.paused = false;
|
|
543
|
+
}
|
|
544
|
+
return 'true';
|
|
545
|
+
},
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
register({
|
|
549
|
+
name: '$stopMusic',
|
|
550
|
+
brackets: 'optional',
|
|
551
|
+
min: 0,
|
|
552
|
+
max: 1,
|
|
553
|
+
signature: '[guildID?]',
|
|
554
|
+
description: 'Hentikan musik dan kosongkan antrean',
|
|
555
|
+
run: async (ctx, [guildId]) => {
|
|
556
|
+
const guild = resolveGuild(ctx, guildId);
|
|
557
|
+
const queue = manager.queues.get(guild.id);
|
|
558
|
+
if (queue) await queue.stop();
|
|
559
|
+
return '';
|
|
560
|
+
},
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
register({
|
|
564
|
+
name: '$skip',
|
|
565
|
+
brackets: 'optional',
|
|
566
|
+
min: 0,
|
|
567
|
+
max: 1,
|
|
568
|
+
signature: '[guildID?]',
|
|
569
|
+
description: 'Lewati lagu yang sedang diputar',
|
|
570
|
+
run: async (ctx, [guildId]) => {
|
|
571
|
+
const guild = resolveGuild(ctx, guildId);
|
|
572
|
+
const queue = manager.queues.get(guild.id);
|
|
573
|
+
if (!queue?.isPlaying) return '';
|
|
574
|
+
const skipped = await queue.skip();
|
|
575
|
+
return skipped?.title ?? '';
|
|
576
|
+
},
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
register({
|
|
580
|
+
name: '$seek',
|
|
581
|
+
brackets: 'required',
|
|
582
|
+
min: 1,
|
|
583
|
+
max: 2,
|
|
584
|
+
signature: '[posisi;guildID?]',
|
|
585
|
+
description: 'Lompat ke posisi tertentu (contoh: 1:30 atau 90)',
|
|
586
|
+
run: async (ctx, [posStr, guildId]) => {
|
|
587
|
+
const guild = resolveGuild(ctx, guildId);
|
|
588
|
+
const queue = manager.queues.get(guild.id);
|
|
589
|
+
if (!queue?.player || !queue.current) {
|
|
590
|
+
throw new BotError('Tidak ada lagu yang sedang diputar');
|
|
591
|
+
}
|
|
592
|
+
if (!queue.current.isSeekable) {
|
|
593
|
+
throw new BotError('Lagu ini tidak mendukung fitur seek (siaran langsung / stream)');
|
|
594
|
+
}
|
|
595
|
+
const ms = parseDurationMs(posStr);
|
|
596
|
+
const target = Math.max(0, Math.min(ms, queue.current.length));
|
|
597
|
+
await queue.player.seekTo(target);
|
|
598
|
+
return formatDuration(target);
|
|
599
|
+
},
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
register({
|
|
603
|
+
name: '$volume',
|
|
604
|
+
brackets: 'optional',
|
|
605
|
+
min: 0,
|
|
606
|
+
max: 2,
|
|
607
|
+
signature: '[angka?;guildID?]',
|
|
608
|
+
description: 'Atur (0-150) atau lihat volume pemutar musik',
|
|
609
|
+
run: async (ctx, [volStr, guildId]) => {
|
|
610
|
+
const guild = resolveGuild(ctx, guildId);
|
|
611
|
+
const queue = manager.queues.get(guild.id);
|
|
612
|
+
if (!queue) return '100';
|
|
613
|
+
|
|
614
|
+
if (has(volStr)) {
|
|
615
|
+
const v = Math.max(0, Math.min(150, parseInt(volStr, 10) || 100));
|
|
616
|
+
queue.volume = v;
|
|
617
|
+
if (queue.player) {
|
|
618
|
+
if (typeof queue.player.setGlobalVolume === 'function') {
|
|
619
|
+
await queue.player.setGlobalVolume(v);
|
|
620
|
+
} else if (typeof queue.player.setVolume === 'function') {
|
|
621
|
+
await queue.player.setVolume(v);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
return String(v);
|
|
625
|
+
}
|
|
626
|
+
return String(queue.volume);
|
|
627
|
+
},
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
register({
|
|
631
|
+
name: '$loop',
|
|
632
|
+
brackets: 'optional',
|
|
633
|
+
min: 0,
|
|
634
|
+
max: 2,
|
|
635
|
+
signature: '[mode?;guildID?]',
|
|
636
|
+
description: 'Atur perulangan: off, track, queue',
|
|
637
|
+
run: (ctx, [mode, guildId]) => {
|
|
638
|
+
const guild = resolveGuild(ctx, guildId);
|
|
639
|
+
const queue = manager.queues.get(guild.id);
|
|
640
|
+
if (!queue) return 'off';
|
|
641
|
+
|
|
642
|
+
if (has(mode)) {
|
|
643
|
+
const m = mode.trim().toLowerCase();
|
|
644
|
+
if (['off', 'none', 'false', '0'].includes(m)) queue.loop = 'off';
|
|
645
|
+
else if (['track', 'song', 'single', '1'].includes(m)) queue.loop = 'track';
|
|
646
|
+
else if (['queue', 'all', 'playlist', '2'].includes(m)) queue.loop = 'queue';
|
|
647
|
+
else throw new BotError(`Mode loop "${mode}" tidak valid. Pilihan: off, track, queue`);
|
|
648
|
+
}
|
|
649
|
+
return queue.loop;
|
|
650
|
+
},
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
// =========================================================================
|
|
654
|
+
// 3. ANTREAN (QUEUE) & PENGATURANNYA
|
|
655
|
+
// =========================================================================
|
|
656
|
+
|
|
657
|
+
register({
|
|
658
|
+
name: '$queue',
|
|
659
|
+
brackets: 'optional',
|
|
660
|
+
min: 0,
|
|
661
|
+
max: 2,
|
|
662
|
+
signature: '[halaman?;guildID?]',
|
|
663
|
+
description: 'Daftar antrean lagu saat ini',
|
|
664
|
+
run: (ctx, [pageStr, guildId]) => {
|
|
665
|
+
const guild = resolveGuild(ctx, guildId);
|
|
666
|
+
const queue = manager.queues.get(guild.id);
|
|
667
|
+
if (!queue || (!queue.current && !queue.tracks.length)) {
|
|
668
|
+
return 'Antrean kosong.';
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
const page = Math.max(1, parseInt(pageStr, 10) || 1);
|
|
672
|
+
const perPage = 10;
|
|
673
|
+
const totalPages = Math.ceil(queue.tracks.length / perPage) || 1;
|
|
674
|
+
const curPage = Math.min(page, totalPages);
|
|
675
|
+
|
|
676
|
+
const lines = [];
|
|
677
|
+
if (queue.current) {
|
|
678
|
+
lines.push(`▶️ **${queue.current.title}** (${formatDuration(queue.current.length)})`);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
const start = (curPage - 1) * perPage;
|
|
682
|
+
const end = start + perPage;
|
|
683
|
+
const pageTracks = queue.tracks.slice(start, end);
|
|
684
|
+
|
|
685
|
+
pageTracks.forEach((t, i) => {
|
|
686
|
+
lines.push(`${start + i + 1}. ${t.title} (${formatDuration(t.length)})`);
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
if (totalPages > 1) {
|
|
690
|
+
lines.push(`\n— Halaman ${curPage}/${totalPages} (Total ${queue.tracks.length} antrean) —`);
|
|
691
|
+
}
|
|
692
|
+
return lines.join('\n');
|
|
693
|
+
},
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
register({
|
|
697
|
+
name: '$queueLength',
|
|
698
|
+
brackets: 'optional',
|
|
699
|
+
min: 0,
|
|
700
|
+
max: 1,
|
|
701
|
+
signature: '[guildID?]',
|
|
702
|
+
description: 'Jumlah lagu di dalam antrean',
|
|
703
|
+
run: (ctx, [guildId]) => {
|
|
704
|
+
const guild = resolveGuild(ctx, guildId);
|
|
705
|
+
const queue = manager.queues.get(guild.id);
|
|
706
|
+
return String(queue?.tracks?.length ?? 0);
|
|
707
|
+
},
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
register({
|
|
711
|
+
name: '$shuffle',
|
|
712
|
+
brackets: 'optional',
|
|
713
|
+
min: 0,
|
|
714
|
+
max: 1,
|
|
715
|
+
signature: '[guildID?]',
|
|
716
|
+
description: 'Acak urutan antrean lagu',
|
|
717
|
+
run: (ctx, [guildId]) => {
|
|
718
|
+
const guild = resolveGuild(ctx, guildId);
|
|
719
|
+
const queue = manager.queues.get(guild.id);
|
|
720
|
+
if (!queue || queue.tracks.length <= 1) return String(queue?.tracks?.length ?? 0);
|
|
721
|
+
|
|
722
|
+
for (let i = queue.tracks.length - 1; i > 0; i--) {
|
|
723
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
724
|
+
[queue.tracks[i], queue.tracks[j]] = [queue.tracks[j], queue.tracks[i]];
|
|
725
|
+
}
|
|
726
|
+
return String(queue.tracks.length);
|
|
727
|
+
},
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
register({
|
|
731
|
+
name: '$clearQueue',
|
|
732
|
+
brackets: 'optional',
|
|
733
|
+
min: 0,
|
|
734
|
+
max: 1,
|
|
735
|
+
signature: '[guildID?]',
|
|
736
|
+
description: 'Hapus semua lagu antrean tanpa menghentikan lagu saat ini',
|
|
737
|
+
run: (ctx, [guildId]) => {
|
|
738
|
+
const guild = resolveGuild(ctx, guildId);
|
|
739
|
+
const queue = manager.queues.get(guild.id);
|
|
740
|
+
if (!queue) return '0';
|
|
741
|
+
const count = queue.tracks.length;
|
|
742
|
+
queue.tracks = [];
|
|
743
|
+
return String(count);
|
|
744
|
+
},
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
register({
|
|
748
|
+
name: '$removeTrack',
|
|
749
|
+
brackets: 'required',
|
|
750
|
+
min: 1,
|
|
751
|
+
max: 2,
|
|
752
|
+
signature: '[nomor;guildID?]',
|
|
753
|
+
description: 'Hapus satu lagu dari antrean berdasarkan nomor urut',
|
|
754
|
+
run: (ctx, [numStr, guildId]) => {
|
|
755
|
+
const guild = resolveGuild(ctx, guildId);
|
|
756
|
+
const queue = manager.queues.get(guild.id);
|
|
757
|
+
if (!queue || !queue.tracks.length) throw new BotError('Antrean sedang kosong');
|
|
758
|
+
|
|
759
|
+
const index = parseInt(numStr, 10) - 1;
|
|
760
|
+
if (Number.isNaN(index) || index < 0 || index >= queue.tracks.length) {
|
|
761
|
+
throw new BotError(`Nomor lagu tidak valid. Antrean hanya memiliki ${queue.tracks.length} lagu`);
|
|
762
|
+
}
|
|
763
|
+
const [removed] = queue.tracks.splice(index, 1);
|
|
764
|
+
return removed?.title ?? '';
|
|
765
|
+
},
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
// =========================================================================
|
|
769
|
+
// 4. INFORMASI LAGU & STATUS
|
|
770
|
+
// =========================================================================
|
|
771
|
+
|
|
772
|
+
register({
|
|
773
|
+
name: '$nowPlaying',
|
|
774
|
+
brackets: 'optional',
|
|
775
|
+
min: 0,
|
|
776
|
+
max: 1,
|
|
777
|
+
signature: '[guildID?]',
|
|
778
|
+
description: 'Judul lagu yang sedang diputar',
|
|
779
|
+
run: (ctx, [guildId]) => {
|
|
780
|
+
const guild = resolveGuild(ctx, guildId);
|
|
781
|
+
const queue = manager.queues.get(guild.id);
|
|
782
|
+
return queue?.current?.title ?? '';
|
|
783
|
+
},
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
register({
|
|
787
|
+
name: '$songTitle',
|
|
788
|
+
brackets: 'optional',
|
|
789
|
+
min: 0,
|
|
790
|
+
max: 1,
|
|
791
|
+
signature: '[guildID?]',
|
|
792
|
+
description: 'Judul lagu yang sedang diputar (alias $nowPlaying)',
|
|
793
|
+
run: (ctx, [guildId]) => {
|
|
794
|
+
const guild = resolveGuild(ctx, guildId);
|
|
795
|
+
const queue = manager.queues.get(guild.id);
|
|
796
|
+
return queue?.current?.title ?? '';
|
|
797
|
+
},
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
register({
|
|
801
|
+
name: '$songDuration',
|
|
802
|
+
brackets: 'optional',
|
|
803
|
+
min: 0,
|
|
804
|
+
max: 1,
|
|
805
|
+
signature: '[guildID?]',
|
|
806
|
+
description: 'Durasi total lagu saat ini (format mm:ss / hh:mm:ss)',
|
|
807
|
+
run: (ctx, [guildId]) => {
|
|
808
|
+
const guild = resolveGuild(ctx, guildId);
|
|
809
|
+
const queue = manager.queues.get(guild.id);
|
|
810
|
+
return queue?.current ? formatDuration(queue.current.length) : '';
|
|
811
|
+
},
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
register({
|
|
815
|
+
name: '$songPosition',
|
|
816
|
+
brackets: 'optional',
|
|
817
|
+
min: 0,
|
|
818
|
+
max: 1,
|
|
819
|
+
signature: '[guildID?]',
|
|
820
|
+
description: 'Posisi pemutaran lagu saat ini (format mm:ss / hh:mm:ss)',
|
|
821
|
+
run: (ctx, [guildId]) => {
|
|
822
|
+
const guild = resolveGuild(ctx, guildId);
|
|
823
|
+
const queue = manager.queues.get(guild.id);
|
|
824
|
+
return queue?.current && queue.player ? formatDuration(queue.player.position ?? 0) : '00:00';
|
|
825
|
+
},
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
register({
|
|
829
|
+
name: '$songURL',
|
|
830
|
+
brackets: 'optional',
|
|
831
|
+
min: 0,
|
|
832
|
+
max: 1,
|
|
833
|
+
signature: '[guildID?]',
|
|
834
|
+
description: 'URL lagu yang sedang diputar',
|
|
835
|
+
run: (ctx, [guildId]) => {
|
|
836
|
+
const guild = resolveGuild(ctx, guildId);
|
|
837
|
+
const queue = manager.queues.get(guild.id);
|
|
838
|
+
return queue?.current?.uri ?? '';
|
|
839
|
+
},
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
register({
|
|
843
|
+
name: '$songAuthor',
|
|
844
|
+
brackets: 'optional',
|
|
845
|
+
min: 0,
|
|
846
|
+
max: 1,
|
|
847
|
+
signature: '[guildID?]',
|
|
848
|
+
description: 'Pembuat/artis lagu yang sedang diputar',
|
|
849
|
+
run: (ctx, [guildId]) => {
|
|
850
|
+
const guild = resolveGuild(ctx, guildId);
|
|
851
|
+
const queue = manager.queues.get(guild.id);
|
|
852
|
+
return queue?.current?.author ?? '';
|
|
853
|
+
},
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
register({
|
|
857
|
+
name: '$songThumbnail',
|
|
858
|
+
brackets: 'optional',
|
|
859
|
+
min: 0,
|
|
860
|
+
max: 1,
|
|
861
|
+
signature: '[guildID?]',
|
|
862
|
+
description: 'URL gambar thumbnail lagu yang sedang diputar',
|
|
863
|
+
run: (ctx, [guildId]) => {
|
|
864
|
+
const guild = resolveGuild(ctx, guildId);
|
|
865
|
+
const queue = manager.queues.get(guild.id);
|
|
866
|
+
return queue?.current?.artworkUrl ?? '';
|
|
867
|
+
},
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
register({
|
|
871
|
+
name: '$songRequester',
|
|
872
|
+
brackets: 'optional',
|
|
873
|
+
min: 0,
|
|
874
|
+
max: 1,
|
|
875
|
+
signature: '[guildID?]',
|
|
876
|
+
description: 'Username pengirim yang meminta lagu saat ini',
|
|
877
|
+
run: (ctx, [guildId]) => {
|
|
878
|
+
const guild = resolveGuild(ctx, guildId);
|
|
879
|
+
const queue = manager.queues.get(guild.id);
|
|
880
|
+
return queue?.current?.requester ?? '';
|
|
881
|
+
},
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
register({
|
|
885
|
+
name: '$isPlaying',
|
|
886
|
+
brackets: 'optional',
|
|
887
|
+
min: 0,
|
|
888
|
+
max: 1,
|
|
889
|
+
signature: '[guildID?]',
|
|
890
|
+
description: 'true/false: apakah bot sedang memutar lagu',
|
|
891
|
+
run: (ctx, [guildId]) => {
|
|
892
|
+
const guild = resolveGuild(ctx, guildId);
|
|
893
|
+
const queue = manager.queues.get(guild.id);
|
|
894
|
+
return String(Boolean(queue?.isPlaying));
|
|
895
|
+
},
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
register({
|
|
899
|
+
name: '$isPaused',
|
|
900
|
+
brackets: 'optional',
|
|
901
|
+
min: 0,
|
|
902
|
+
max: 1,
|
|
903
|
+
signature: '[guildID?]',
|
|
904
|
+
description: 'true/false: apakah musik sedang dijeda',
|
|
905
|
+
run: (ctx, [guildId]) => {
|
|
906
|
+
const guild = resolveGuild(ctx, guildId);
|
|
907
|
+
const queue = manager.queues.get(guild.id);
|
|
908
|
+
return String(Boolean(queue?.paused));
|
|
909
|
+
},
|
|
910
|
+
});
|
|
911
|
+
register({
|
|
912
|
+
name: '$songRemaining',
|
|
913
|
+
brackets: 'optional',
|
|
914
|
+
min: 0,
|
|
915
|
+
max: 1,
|
|
916
|
+
signature: '[guildID?]',
|
|
917
|
+
description: 'Sisa durasi lagu yang sedang diputar (format mm:ss / hh:mm:ss)',
|
|
918
|
+
run: (ctx, [guildId]) => {
|
|
919
|
+
const guild = resolveGuild(ctx, guildId);
|
|
920
|
+
const queue = manager.queues.get(guild.id);
|
|
921
|
+
if (!queue?.current || !queue.player) return '00:00';
|
|
922
|
+
const pos = queue.player.position ?? 0;
|
|
923
|
+
const remaining = Math.max(0, queue.current.length - pos);
|
|
924
|
+
return formatDuration(remaining);
|
|
925
|
+
},
|
|
926
|
+
});
|
|
927
|
+
|
|
928
|
+
register({
|
|
929
|
+
name: '$voiceChannelName',
|
|
930
|
+
brackets: 'optional',
|
|
931
|
+
min: 0,
|
|
932
|
+
max: 1,
|
|
933
|
+
signature: '[guildID?]',
|
|
934
|
+
description: 'Nama voice channel tempat bot berada saat ini',
|
|
935
|
+
run: (ctx, [guildId]) => {
|
|
936
|
+
const guild = resolveGuild(ctx, guildId);
|
|
937
|
+
const chId = guild.members?.me?.voice?.channelId;
|
|
938
|
+
if (!chId) return '';
|
|
939
|
+
const ch = guild.channels?.cache?.get(chId);
|
|
940
|
+
return ch?.name ?? '';
|
|
941
|
+
},
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
register({
|
|
945
|
+
name: '$isUserInVoice',
|
|
946
|
+
brackets: 'optional',
|
|
947
|
+
min: 0,
|
|
948
|
+
max: 2,
|
|
949
|
+
signature: '[userID?;guildID?]',
|
|
950
|
+
description: 'true/false: apakah user sedang berada di voice channel',
|
|
951
|
+
run: (ctx, [userId, guildId]) => {
|
|
952
|
+
const guild = resolveGuild(ctx, guildId);
|
|
953
|
+
const member = has(userId) ? guild.members?.cache?.get(String(userId).trim()) : ctx.member;
|
|
954
|
+
return String(Boolean(member?.voice?.channelId));
|
|
955
|
+
},
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
register({
|
|
959
|
+
name: '$userVoiceChannelID',
|
|
960
|
+
brackets: 'optional',
|
|
961
|
+
min: 0,
|
|
962
|
+
max: 2,
|
|
963
|
+
signature: '[userID?;guildID?]',
|
|
964
|
+
description: 'ID voice channel tempat user berada saat ini',
|
|
965
|
+
run: (ctx, [userId, guildId]) => {
|
|
966
|
+
const guild = resolveGuild(ctx, guildId);
|
|
967
|
+
const member = has(userId) ? guild.members?.cache?.get(String(userId).trim()) : ctx.member;
|
|
968
|
+
return member?.voice?.channelId ?? '';
|
|
969
|
+
},
|
|
970
|
+
});
|
|
971
|
+
|
|
972
|
+
register({
|
|
973
|
+
name: '$voiceMembersCount',
|
|
974
|
+
brackets: 'optional',
|
|
975
|
+
min: 0,
|
|
976
|
+
max: 2,
|
|
977
|
+
signature: '[channelID?;guildID?]',
|
|
978
|
+
description: 'Jumlah member yang berada di voice channel',
|
|
979
|
+
run: (ctx, [channelId, guildId]) => {
|
|
980
|
+
const guild = resolveGuild(ctx, guildId);
|
|
981
|
+
const targetId = has(channelId) ? String(channelId).trim() : guild.members?.me?.voice?.channelId;
|
|
982
|
+
if (!targetId) return '0';
|
|
983
|
+
const channel = guild.channels?.cache?.get(targetId);
|
|
984
|
+
return String(channel?.members?.size ?? 0);
|
|
985
|
+
},
|
|
986
|
+
});
|
|
987
|
+
|
|
988
|
+
register({
|
|
989
|
+
name: '$voicePing',
|
|
990
|
+
brackets: 'optional',
|
|
991
|
+
min: 0,
|
|
992
|
+
max: 1,
|
|
993
|
+
signature: '[guildID?]',
|
|
994
|
+
description: 'Ping koneksi audio Lavalink ke Discord (ms)',
|
|
995
|
+
run: (ctx, [guildId]) => {
|
|
996
|
+
const guild = resolveGuild(ctx, guildId);
|
|
997
|
+
const queue = manager.queues.get(guild.id);
|
|
998
|
+
const ping = queue?.player?.ping ?? -1;
|
|
999
|
+
return String(ping >= 0 ? ping : 0);
|
|
1000
|
+
},
|
|
1001
|
+
});
|
|
1002
|
+
|
|
1003
|
+
register({
|
|
1004
|
+
name: '$lavalinkStatus',
|
|
1005
|
+
brackets: 'none',
|
|
1006
|
+
description: 'Status koneksi node Lavalink (connected / disconnected)',
|
|
1007
|
+
run: () => {
|
|
1008
|
+
if (!manager.shoukaku || !manager.options.nodes.length) return 'disconnected';
|
|
1009
|
+
const ideal = manager.shoukaku.getIdealNode();
|
|
1010
|
+
return ideal ? 'connected' : 'disconnected';
|
|
1011
|
+
},
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
/**
|
|
1017
|
+
* Plugin Voice & Musik untuk Bot.
|
|
1018
|
+
*/
|
|
1019
|
+
function voice(options = {}) {
|
|
1020
|
+
return {
|
|
1021
|
+
name: 'voice',
|
|
1022
|
+
install(bot) {
|
|
1023
|
+
const manager = new VoiceManager(bot, options);
|
|
1024
|
+
manager.installFunctions();
|
|
1025
|
+
manager.hookBot();
|
|
1026
|
+
},
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
module.exports = { voice, VoiceManager };
|