This commit is contained in:
Elysia
2024-01-14 12:33:11 +07:00
parent e15b9ab7fe
commit 039dd34cf2
27 changed files with 1250 additions and 5297 deletions

View File

@@ -1,187 +0,0 @@
### Credit: Discord-S.C.U.M
- Link: [Here](https://github.com/Merubokkusu/Discord-S.C.U.M/blob/master/docs/using/fetchingGuildMembers.md#Algorithm)
# Fetching Guild Members
### <strong>Assume you don't have one of the following permissions</strong>
> `ADMINISTRATOR`, `KICK_MEMBERS`, `BAN_MEMBERS`, `MANAGE_ROLES`
Alright so this really needs a page of its own because it's special. There's no actual api endpoint to get the guild members, so instead you have 2 options:
1) fetch the member list sidebar (guild.members.fetchMemberList, which uses opcode 14)
- when to use: if you can see any categories/channels in the guild
- pro: fast
- con: for large servers (```guild.memberCount > 1000```), only not-offline members are fetched
2) search for members by query (guild.members.fetchBruteforce, which uses opcode 8)
- when to use: if you cannot see any categories/channels in the guild
- pro: can potentially get the entire member list, can scrape members from multiple guilds at the same time
- con: slow af (speed is dependent on brute forcer optimizations)
____________________________________
# Links/Table of Contents
- [fetch the member list sidebar (faster, but less members)](#fetch-the-member-list-sidebar)
- [Algorithm](#Algorithm)
- [How many members can I fetch?](#how-many-members-can-i-fetch)
- [Examples](#Examples)
- [Efficiency & Effectiveness](#efficiency--effectiveness)
- [POC: fetching the memberlist backwards](#fetching-the-member-list-backwards)
- [search for members by query (slower, but more members)](#search-for-members-by-query)
- [Usage](#Usage)
- [Algorithm](#Algorithm-1)
- [How many members can I fetch?](#how-many-members-can-i-fetch-1)
___________________________________
## Fetch the Member List Sidebar
#### Algorithm
1) load guild data (send an op14 with range [0,99]). If the guild is unavailable, discord will send over a GUILD_CREATE event.
2) subscribe to a list of ranges in member list sidebar.
3) after a GUILD_MEMBER_LIST_UPDATE is received, update the saved member list data and subscribe to a new list of ranges
note:
- you don't have to wait for a GUILD_MEMBER_LIST_UPDATE event to send the next list of member ranges
- there're 2 methods to fetch the member list:
- overlap. Ranges subscribed to (in order) are:
```
[[0,99], [100,199]]
[[0,99], [100,199], [200,299]]
[[0,99], [200,299], [300,399]]
...
```
- nonoverlap. Ranges subscribed to (in order) are:
```
[[0,99], [100,199]]
[[0,99], [200,299], [300,399]]
[[0,99], [400,499], [500,599]]
...
```
- more info: https://arandomnewaccount.gitlab.io/discord-unofficial-docs/lazy_guilds.html
#### How many members can I fetch?
Even though it's not yet known how discord calculates this, you can still come up with a "ground truth" number. The steps are as follows:
1) open your browser's dev tools (chrome dev tools is a favorite)
2) click on the network tab and make sure you can see websocket connections
3) go to a guild and scroll all the way down on the member list
4) see what are the ranges of the last gateway request your client sends (the # of fetchable members is somewhere in these ranges)
#### Examples
all examples shown use the "overlap" method
```js
const guild = client.guilds.cache.get('id');
const channel = guild.channels.cache.get('id');
// Overlap (slow)
for (let index = 0; index <= guild.memberCount; index += 100) {
await guild.members.fetchMemberList(channel, index, index !== 100).catch(() => {});
await client.sleep(500);
}
// Non-overlap (fast)
for (let index = 0; index <= guild.memberCount; index += 200) {
await guild.members.fetchMemberList(channel, index == 0 ? 100 : index, index !== 100).catch(() => {});
await client.sleep(500);
}
console.log(guild.members.cache.size); // will print the number of members in the guild
```
It's possible that fetchMembers doesn't fetch all not-offline members due to rate limiting. Don't worry if this happens, you can start fetching members from any index.
```js
const guild = client.guilds.cache.get('id');
const channel = guild.channels.cache.get('id');
// Fetch member range 5000-5099
await guild.members.fetchMemberList(channel, 5000);
```
#### Efficiency & Effectiveness
| | overlap&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; | no overlap |
|------|---------|------------|
| 2.1k |![a](https://github.com/Merubokkusu/Discord-S.C.U.M/raw/master/docs/using/memberFetchingStats/2100a.jpg) |![c](https://github.com/Merubokkusu/Discord-S.C.U.M/raw/master/docs/using/memberFetchingStats/2100b.jpg) |
| 128k |![b](https://github.com/Merubokkusu/Discord-S.C.U.M/raw/master/docs/using/memberFetchingStats/128ka.jpg) |![d](https://github.com/Merubokkusu/Discord-S.C.U.M/raw/master/docs/using/memberFetchingStats/128kb.jpg) |
As you can see, the "no overlap" method fetches 200 members/second while the "overlap" method fetches 100 members/second. However, "no overlap" is also a lot less effective. After doing a few more tests with both methods ("overlap" and "no overlap"), "no overlap" shows a lot less consistency/reliability than "overlap".
#### Fetching the member list backwards
(and in pretty much any "style" you want)
So, this is more proof-of-concept, but here's a short explanation.
Suppose you're in a guild with 1000 members and want to fetch the member list backwards (I dunno...more undetectable since noone fetches it backwards? lol).
Since discum requests members in 200-member chunks, you'll either have to request for the following range groups (safer):
```
[[0,99],[800,899],[900,999]] #target start: 800
[[0,99],[700,799],[800,899]] #target start: 700
[[0,99],[600,699],[700,799]] #target start: 600
[[0,99],[500,599],[600,699]] #target start: 500
[[0,99],[400,499],[500,599]] #target start: 400
[[0,99],[300,399],[400,499]] #target start: 300
[[0,99],[200,299],[300,399]] #target start: 200
[[0,99],[100,199],[200,299]] #target start: 100
[[0,99],[100,199]] #target start: 0
```
or the following range groups (faster):
```
[[0,99],[800,899],[900,999]] #target start: 800
[[0,99],[600,699],[700,799]] #target start: 600
[[0,99],[400,499],[500,599]] #target start: 400
[[0,99],[200,299],[300,399]] #target start: 200
[[0,99],[100,199]] #target start: 0
```
The first one looks like an overlap method while the second looks like a no-overlap method. However, since we're fetching the memberlist backwards, we cannot
use 100 and 200 for the methods. Instead, we need a list of multipliers (method) and a startIndex.
____________________________________
## Search for Members by Query
#### Usage
1) run the function:
```js
guild.members.fetchBruteforce({
delay: 500,
depth: 1, // ['a', 'b', 'c', 'd', ...] or ['aa', 'ab', 'ac', 'ad', ...] if depth is 2, ...
})
```
A wait time of at least 0.5 is needed to prevent the brute forcer from rate limiting too often. In the event that the brute forcer does get rate limited, some time will be lost reconnecting.
#### Algorithm
for simplicity, assume that the list of characters to search for is ['a', 'b', 'c', 'd']
1) query for up to 100 members in guild who have a nickname/username starting with 'a'
2) on a GUILD_MEMBERS_CHUNK event:
- if there are 100 results:
- add on the 2nd character of the last result. For example, if the results are
```
aaaaaaaaaaaa
aaadfd3fgdftjh
...
Acaddd
```
,
the next query will be 'ac'. Note: searches are case-insensitive and consecutive spaces are treated like single spaces.
- if there are less than 100 results:
- replace the last index of the query with the next option in the list
This algorithm can definitely be made a lot better so have at it. The brute forcer example is just there to help you get started.
#### How many members can I fetch?
- a limit is posed if many users have the same nickname & username (but different discriminators). Only the 1st 100 members will be able to be fetched. There's no known way to include the discriminator # in the search.
- also, in order to query users with fancy characters in their username/nickname, the op8 brute forcer needs to be slowed down (cause, more characters to search)
#### Testing !!!
- Example
```js
const Discord = require('discord.js-selfbot-v13');
const client = new Discord.Client();
client.on('debug', console.log)
client.on('ready', () => {
console.log('Logged in as', client.user.tag);
const guild = client.guilds.cache.get('662267976984297473'); // Midjourney - 13M members
guild.members.fetchBruteforce({
depth: 2, // 2 levels of recursion
delay: 500, // 500ms delay between each request
});
setInterval(() => {
console.log('Fetching members...', guild.members.cache.size);
}, 1000);
});
client.login('token');
```
- 2000 years later...
<img src='https://cdn.discordapp.com/attachments/820557032016969751/1090606227265966140/image.png'>
`138k/13.8M (1%)` members fetched in `~30 mins` (with a delay of 500ms) :skull:

View File

@@ -17,40 +17,10 @@
## Interaction
<details open>
<summary>Button Click</summary>
```js
await Button.click(Message);
//
await message.clickButton(buttonID);
//
await message.clickButton(); // first button
//
await message.clickButton({ row: 0, col: 0})
```
</details>
<details open>
<summary>Message Select Menu</summary>
```js
await MessageSelectMenu.select(Message, options); // (v1)
// value: ['value1', 'value2' , ...]
await message.selectMenu(menuID, options) // If message has >= 2 menu
await message.selectMenu(options) // If message has 1 menu
```
</details>
<details open>
<summary>Slash Command</summary>
### [Click here](https://github.com/aiko-chan-ai/discord.js-selfbot-v13/blob/main/Document/SlashCommand.md)
</details>
<details open>
<summary>Message Context Command</summary>
```js
await message.contextMenu(botID, commandName);
```
</details>
<details open>
@@ -64,13 +34,7 @@ await message.contextMenu(botID, commandName);
Code:
```js
const Discord = require('discord.js-selfbot-v13');
// Selfhost WebEmbed: https://github.com/aiko-chan-ai/WebEmbed
const w = new Discord.WebEmbed({
shorten: true,
hidden: false, // if you send this embed with MessagePayload.options.embeds, it must set to false
baseURL: '', // if you want self-host API, else skip :v
shortenAPI: '', // if you want Custom shortenAPI (Method: Get, response: Text => URL), else skip :v
})
const w = new Discord.WebEmbed()
.setAuthor({ name: 'hello', url: 'https://google.com' })
.setColor('RED')
.setDescription('description uh')
@@ -83,7 +47,7 @@ const w = new Discord.WebEmbed({
.setVideo(
'https://cdn.discordapp.com/attachments/877060758092021801/957691816143097936/The_Quintessential_Quintuplets_And_Rick_Astley_Autotune_Remix.mp4',
);
message.channel.send({ content: `Hello world`, embeds: [w] }) // Patched :)
message.channel.send({ content: `Hello world ${Discord.WebEmbed.hiddenEmbed} ${w}` });
```
### Features & Issues

View File

@@ -1,10 +1,3 @@
## Setup
```js
const client = new Client({
syncStatus: false,
});
```
## Custom Status and RPC
<strong>Custom Status</strong>
@@ -68,57 +61,7 @@ client.user.setActivity(r);
> Tutorial:
## Method 1:
+ Step 1: Send image to Discord
<img src='https://cdn.discordapp.com/attachments/820557032016969751/995297572732284968/unknown.png'>
+ Step 2: Get Image URL
<img src='https://cdn.discordapp.com/attachments/820557032016969751/995298082474426418/unknown.png'>
```sh
Demo URL: https://cdn.discordapp.com/attachments/820557032016969751/991172011483218010/unknown.png
```
+ Step 3: Replace `https://cdn.discordapp.com/` or `https://media.discordapp.net/` with `mp:`
```diff
- https://cdn.discordapp.com/attachments/820557032016969751/991172011483218010/unknown.png
- https://media.discordapp.net/attachments/820557032016969751/991172011483218010/unknown.png
+ mp:attachments/820557032016969751/991172011483218010/unknown.png
```
+ Step 4:
```js
const Discord = require('discord.js-selfbot-v13');
const r = new Discord.RichPresence()
.setApplicationId('817229550684471297')
.setType('PLAYING')
.setURL('https://youtube.com/watch?v=dQw4w9WgXcQ')
.setState('State')
.setName('Name')
.setDetails('Details')
.setParty({
max: 9,
current: 1,
id: Discord.getUUID(),
})
.setStartTimestamp(Date.now())
.setAssetsLargeImage('mp:attachments/820557032016969751/991172011483218010/unknown.png')
.setAssetsLargeText('Youtube')
.setAssetsSmallImage('895316294222635008')
.setAssetsSmallText('Bot')
.addButton('name', 'https://link.com/')
client.user.setActivity(r);
```
## Method 2: (Discord URL, 2.3.78+)
## Method 1: (Discord URL, v2.3.78+)
```js
const Discord = require('discord.js-selfbot-v13');
@@ -145,7 +88,7 @@ client.user.setActivity(r);
<img src='https://cdn.discordapp.com/attachments/820557032016969751/995301015257616414/unknown.png'>
## Method 3 (Custom URL, 2.3.78+)
## Method 2 (Custom URL, 2.3.78+)
```js
const Discord = require('discord.js-selfbot-v13');

View File

@@ -42,10 +42,3 @@ await message.channel.sendSlash('718642000898818048', 'sauce', a)
### Result
![image](https://user-images.githubusercontent.com/71698422/173347075-5c8a1347-3845-489e-956b-63975911b6e0.png)
# Events
- [interactionCreate](https://discordjs-self-v13.netlify.app/#/docs/docs/main/class/Client?scrollTo=e-interactionCreate)
- [interactionFailure](https://discordjs-self-v13.netlify.app/#/docs/docs/main/class/Client?scrollTo=e-interactionFailure)
- [interactionSuccess](https://discordjs-self-v13.netlify.app/#/docs/docs/main/class/Client?scrollTo=e-interactionSuccess)
- [interactionModalCreate](https://discordjs-self-v13.netlify.app/#/docs/docs/main/class/Client?scrollTo=e-interactionModalCreate)

View File

@@ -1,170 +0,0 @@
# Quick Links:
- [Setting](https://github.com/aiko-chan-ai/discord.js-selfbot-v13/blob/main/Document/User.md#user-settings)
- [User Info](https://github.com/aiko-chan-ai/discord.js-selfbot-v13/blob/main/Document/User.md#discord-user-info)
- [Relationship](https://github.com/aiko-chan-ai/discord.js-selfbot-v13/blob/main/Document/User.md#discord-user-friend--blocked)
- [Other](https://github.com/aiko-chan-ai/discord.js-selfbot-v13/blob/main/Document/User.md#user--clientuser-method)
## User Settings
<details open>
<summary><strong>Click to show</strong></summary>
```js
client.setting // Return Data Setting User;
client.settings.setDisplayCompactMode(true | false); // Message Compact Mode
client.settings.setTheme('dark' | 'light'); // Discord App theme
client.settings.setLocale(value); // Set Language
/**
* * Locale Setting, must be one of:
* * `DANISH`
* * `GERMAN`
* * `ENGLISH_UK`
* * `ENGLISH_US`
* * `SPANISH`
* * `FRENCH`
* * `CROATIAN`
* * `ITALIAN`
* * `LITHUANIAN`
* * `HUNGARIAN`
* * `DUTCH`
* * `NORWEGIAN`
* * `POLISH`
* * `BRAZILIAN_PORTUGUESE`
* * `ROMANIA_ROMANIAN`
* * `FINNISH`
* * `SWEDISH`
* * `VIETNAMESE`
* * `TURKISH`
* * `CZECH`
* * `GREEK`
* * `BULGARIAN`
* * `RUSSIAN`
* * `UKRAINIAN`
* * `HINDI`
* * `THAI`
* * `CHINA_CHINESE`
* * `JAPANESE`
* * `TAIWAN_CHINESE`
* * `KOREAN`
*/
// Setting Status
client.settings.setCustomStatus({
status: 'online', // 'online' | 'idle' | 'dnd' | 'invisible' | null
text: 'Hello world', // String | null
emoji: '🎮', // UnicodeEmoji | DiscordEmoji | null
expires: null, // Date.now() + 1 * 3600 * 1000 <= 1h to ms
});
// => Clear
client.settings.setCustomStatus();
```
</details>
## Discord User Info
<details open>
<summary><strong>Click to show</strong></summary>
Code:
```js
GuildMember.user.getProfile();
// or
User.getProfile();
```
Response
```js
User {
id: '721746046543331449',
bot: false,
system: false,
flags: UserFlagsBitField { bitfield: 256 },
connectedAccounts: [],
premiumSince: 1623357181151,
premiumGuildSince: 0,
bio: null,
mutualGuilds: Collection(3) [Map] {
'906765260017516605' => { id: '906765260017516605', nick: null },
'809133733591384155' => { id: '809133733591384155', nick: 'uwu' },
'926065180788531261' => { id: '926065180788531261', nick: 'shiro' }
},
username: 'Shiraori',
discriminator: '1782',
avatar: 'f9ba7fb35b223e5f1a12eb910faa40c2',
banner: undefined,
accentColor: undefined
}
```
</details>
## Discord User Friend / Blocked
<details open>
<summary><strong>Click to show</strong></summary>
Code:
```js
// You can use client.relationships to manage your friends and blocked users.
GuildMember.user.setFriend();
User.unFriend();
Message.member.user.sendFriendRequest();
// or
GuildMember.user.setBlock();
User.unBlock();
```
Response
```js
User {
id: '721746046543331449',
bot: false,
system: false,
flags: UserFlagsBitField { bitfield: 256 },
note: null,
connectedAccounts: [],
premiumSince: 1623357181151,
premiumGuildSince: 0,
bio: null,
mutualGuilds: Collection(3) [Map] {
'906765260017516605' => { id: '906765260017516605', nick: null },
'809133733591384155' => { id: '809133733591384155', nick: 'uwu' },
'926065180788531261' => { id: '926065180788531261', nick: 'shiro' }
},
username: 'Shiraori',
discriminator: '1782',
avatar: 'f9ba7fb35b223e5f1a12eb910faa40c2',
banner: undefined,
accentColor: undefined
}
```
</details>
## User & ClientUser Method
<details open>
<summary><strong>Click to show</strong></summary>
```js
// HypeSquad
await client.user.setHypeSquad('HOUSE_BRAVERY');
await client.user.setHypeSquad('HOUSE_BRILLIANCE');
await client.user.setHypeSquad('HOUSE_BALANCE');
await client.user.setHypeSquad('LEAVE');
// Set Note to User
await user.setNote('Hello World');
// Set Username
await client.user.setUsername('new username', 'password');
// Set Accent Color
await client.user.setAccentColor('RED'); // set color same as Embed.setColor()
// Set Banner
await client.user.setBanner('image file / image url'); // same as setAvatar & Require Nitro level 2
// Set Discord Tag
await client.user.setDiscriminator('1234', 'password'); // #1234 & Require Nitro
// Set About me
await client.user.setAboutMe('Hello World');
// Set Email
await client.user.setEmail('aiko.dev@mail.nezukobot.vn', 'password'); // It is clone email =))
// Change Password
await client.user.setPassword('old password', 'new password');
// Disable Account
await client.user.disableAccount('password');
// Delete Account [WARNING] Cannot be changed once used!
await client.user.deleteAccount('password');
// Redeem Nitro
await client.redeemNitro('code')
```
</details>

View File

@@ -1,628 +0,0 @@
# Setup
- Before you use it, properly initialize the module (`@discordjs/voice` patch)
```js
new Client({
patchVoice: true,
})
```
# Usage: Call DM / Group DM
```js
const dmChannel = client.channels.cache.get('id');
/* or
const dmChannel = User.dmChannel || await User.createDM();
*/
const connection = await dmChannel.call();
/* Return @discordjs/voice VoiceConnection */
```
# Play Music using `play-dl`
```js
const play = require('play-dl');
const {
createAudioPlayer,
createAudioResource,
NoSubscriberBehavior,
} = require('@discordjs/voice');
const channel = (await (message.member.user.dmChannel || message.member.user.createDM()));
const connection = channel.voiceConnection || await channel.call();
let stream;
if (!args[0]) {
return message.channel.send('Enter something to search for.');
} else if (args[0].startsWith('https://www.youtube.com/watch?v=')) {
stream = await play.stream(args.join(' '));
} else {
const yt_info = await play.search(args, {
limit: 1
});
stream = await play.stream(yt_info[0].url);
}
const resource = createAudioResource(stream.stream, {
inputType: stream.type,
inlineVolume: true,
});
resource.volume.setVolume(0.25);
const player = createAudioPlayer({
behaviors: {
noSubscriber: NoSubscriberBehavior.Play,
},
});
let i = setInterval(() => {
const m = channel.voiceUsers.get(message.author.id);
if (m) {
player.play(resource);
connection.subscribe(player);
clearInterval(i);
}
else console.log('waiting for voice connection');
}, 250);
```
# Play Music with module (support Play, Pause, Search, Skip, Previous, Volume, Loop)
```js
/* Copyright aiko-chan-ai @2022. All rights reserved. */
const DjsVoice = require('@discordjs/voice');
const Discord = require('discord.js-selfbot-v13');
const playDL = require('play-dl');
const EventEmitter = require('events');
const Event = {
READY: 'ready',
NO_SEARCH_RESULT: 'searchNoResult',
SEARCH_RESULT: 'searchResult',
PLAY_SONG: 'playSong',
ADD_SONG: 'addSong',
ADD_PLAYLIST: 'addPlaylist',
LEAVE_VC: 'disconnect',
FINISH: 'finish',
EMPTY: 'empty',
ERROR: 'error',
}
class Stack {
constructor() {
this.data = [];
}
push(item) {
return this.data.push(item);
}
pop() {
return this.data.pop();
}
peek() {
return this.data[this.length - 1];
}
get length() {
return this.data.length;
}
isEmpty() {
return this.length === 0;
}
reset () {
this.data = [];
return this;
}
}
class Queue {
constructor() {
this.data = [];
}
enqueue(item) {
return this.data.unshift(item);
}
dequeue() {
return this.data.pop();
}
peek() {
return this.data[this.length - 1];
}
get length() {
return this.data.length;
}
isEmpty() {
return this.data.length === 0;
}
reset() {
this.data = [];
return this;
}
}
class Player extends EventEmitter {
constructor(client, options = {}) {
super();
if (!client || !client instanceof Discord.Client) throw new Error('Invalid Discord Client (Selfbot)');
Object.defineProperty(this, 'client', { value: client });
this._playDl = playDL;
this._queue = new Queue();
this._previousSongs = new Stack();
this.song = null;
this.guild = undefined;
this.channel = undefined;
this._currentResourceAudio = undefined;
this._currentTime = 0;
this._playingTime = null;
this.isPlaying = false;
this.volume = 100;
this.loopMode = 0;
this.message = undefined;
this._timeoutEmpty = undefined;
this._player = DjsVoice.createAudioPlayer({
behaviors: {
noSubscriber: DjsVoice.NoSubscriberBehavior.Play,
},
});
this._playerEvent();
this._validateOptions(options);
this._discordEvent();
this._privateEvent();
}
get currentTime() {
return this._currentTime || Date.now() - this._playingTime;
}
get currentConnection() {
return DjsVoice.getVoiceConnection(this.guild?.id || null);
}
get queue() {
return this._queue.data;
}
get previousSongs() {
return this._previousSongs.data;
}
authorization() {
this._playDl.authorization();
}
_validateOptions(options) {
if (typeof options !== 'object') throw new Error(`Invalid options type (Required: object, got: ${typeof options})`);
this.options = {
directLink: true,
joinNewVoiceChannel: true,
waitingUserToPlayInDMs: true,
nsfw: false,
leaveOnEmpty: true,
leaveOnFinish: true,
leaveOnStop: true,
savePreviousSongs: true,
emptyCooldown: 10_000,
}
if (typeof options.directLink === 'boolean') {
this.options.directLink = options.directLink;
}
if (typeof options.joinNewVoiceChannel === 'boolean') {
this.options.joinNewVoiceChannel = options.joinNewVoiceChannel;
}
if (typeof options.waitingUserToPlayInDMs === 'boolean') {
this.options.waitingUserToPlayInDMs = options.waitingUserToPlayInDMs;
}
if (typeof options.nsfw === 'boolean') {
this.options.nsfw = options.nsfw;
}
if (typeof options.leaveOnEmpty === 'boolean') {
if (typeof options.emptyCooldown === 'number') {
this.options.leaveOnEmpty = options.leaveOnEmpty;
this.options.emptyCooldown = options.emptyCooldown;
} else {
this.options.leaveOnEmpty = false;
}
}
if (typeof options.leaveOnFinish === 'boolean') {
this.options.leaveOnFinish = options.leaveOnFinish;
}
if (typeof options.leaveOnStop === 'boolean') {
this.options.leaveOnStop = options.leaveOnStop;
}
if (typeof options.savePreviousSongs === 'boolean') {
this.options.savePreviousSongs = options.savePreviousSongs;
}
}
async play(options = {}) {
const {
message,
channel,
query,
} = options;
if (!(message instanceof Discord.Message)) throw new Error(`Invalid message type (Required: Message, got: ${typeof message})`);
if (channel &&
(
channel instanceof Discord.DMChannel ||
channel instanceof Discord.PartialGroupDMChannel ||
channel instanceof Discord.VoiceChannel ||
channel instanceof Discord.StageChannel
)
) {
let checkChangeVC = false;
if (!this.channel) this.channel = channel;
else {
if (this.options.joinNewVoiceChannel) {
if (this.channel.id !== channel.id) checkChangeVC = true;
this.channel = channel;
}
}
this.guild = channel.guild;
this.message = message;
if (typeof query !== 'string') throw new Error(`Invalid query type (Required: string, got: ${typeof query})`);
const result = await this.search(message, query);
if (result.length < 1) {
throw new Error('No search result with the given query: ' + query);
} else {
for (let i = 0; i < result.length; i++) {
this._queue.enqueue(result[i]);
}
if (!this.isPlaying) {
this._skip(checkChangeVC);
} else if (!result[0].playlist) {
this.emit(Event.ADD_SONG, result[0]);
}
}
} else {
throw new Error(`Invalid channel. Make sure the channel is a DMChannel | PartialGroupDMChannel | VoiceChannel | StageChannel.`);
}
}
async search(message, query, limit = 1) {
if (!(message instanceof Discord.Message)) throw new Error(`Invalid message type (Required: Message, got: ${typeof message})`);
if (typeof query !== 'string') throw new Error(`Invalid query type (Required: string, got: ${typeof query})`);
if (typeof limit !== 'number') throw new Error(`Invalid limit type (Required: number, got: ${typeof limit})`);
if (limit < 1) {
limit = 1;
process.emitWarning(`Invalid limit value (Required: 1 or more, got: ${limit})`);
};
if (limit > 10) {
limit = 10;
process.emitWarning(`Invalid limit value (Required: 10 or less, got: ${limit})`);
};
if (/^(https?\:\/\/)?(www\.youtube\.com|youtu\.be)\/.+$/.test(query)) {
const validateData = this._playDl.yt_validate(query);
if (validateData == 'video') {
const result = await this._playDl.video_info(query);
return [result.video_details];
} else if (validateData == 'playlist') {
const result = await this._playDl.playlist_info(query);
this.emit(Event.ADD_PLAYLIST, result);
const allVideo = await result.all_videos();
return allVideo.map(video => {
Object.defineProperty(video, 'playlist', { value: result });
return video;
});
} else {
return this.emit(Event.ERROR, new Error('Invalid YouTube URL: ' + query));
}
} else {
const result = await this._playDl.search(query, {
limit,
unblurNSFWThumbnails: this.options.nsfw,
});
if (result.length < 1) {
this.emit(Event.NO_SEARCH_RESULT, message, query, limit);
return [];
} else {
this.emit(Event.SEARCH_RESULT, message, result, query, limit);
return result;
}
}
}
setLoopMode(mode) {
if ([0, 1, 2].includes(mode)) {
this._loopMode = mode;
} else {
throw new Error(`Invalid mode value (Required: 0 [No loop], 1 [Loop song], 2 [Loop queue], got: ${mode})`);
}
}
async createStream(url) {
const stream = await this._playDl.stream(url);
const resource = DjsVoice.createAudioResource(stream.stream, {
inputType: stream.type,
inlineVolume: true,
});
this._currentResourceAudio = resource;
this.setVolume(this.volume);
}
_play() {
this._player.play(this._currentResourceAudio);
}
setVolume(volume) {
if (!this._currentResourceAudio) throw new Error('No current resource audio');
if (typeof volume !== 'number') throw new Error(`Invalid volume type (Required: number, got: ${typeof volume})`);
if (volume < 0) {
volume = 0;
process.emitWarning(`Invalid volume value (Required: 0 or more, got: ${volume})`);
} else if (volume > 100) {
volume = 100;
process.emitWarning(`Invalid volume value (Required: 100 or less, got: ${volume})`);
}
this._currentResourceAudio.volume.setVolume((volume / 100).toFixed(2));
this.volume = (volume / 100).toFixed(2) * 100;
return (volume / 100).toFixed(2) * 100;
}
pause() {
if (!this._currentResourceAudio) throw new Error('No current resource audio');
this._player.pause();
}
resume() {
if (!this._currentResourceAudio) throw new Error('No current resource audio');
this._player.unpause();
}
stop() {
if (!this._currentResourceAudio) throw new Error('No current resource audio');
this._stop(false, this.options.leaveOnStop);
}
_reset(){
this._currentTime = 0;
this._currentResourceAudio = null;
this._playingTime = 0;
this.isPlaying = false;
this._player.stop();
}
_stop(finish = false, force = false) {
if (!this._currentResourceAudio) return;
this._queue.reset();
this._previousSongs.reset();
this._timeoutEmpty = undefined;
this._reset();
if (force || finish && this.options.leaveOnFinish) this.currentConnection?.destroy();
this.channel = null;
this.guild = null;
this.song = null;
this.volume = 100;
this.loopMode = 0;
this.message = null;
}
skip() {
this._skip();
}
async _skip(checkChangeVC = false) {
if (!this._queue.length) throw new Error('No song in the queue');
const currentSong = this.song;
if (this.loopMode == 0) {
if (this.options.savePreviousSongs) this._previousSongs.push(currentSong);
const nextSong = this._queue.dequeue();
this.song = nextSong;
} else if (this.loopMode == 1) {
this.song = currentSong;
} else if (this.loopMode == 2) {
this._queue.enqueue(currentSong);
const nextSong = this._queue.dequeue();
this.song = nextSong;
}
await this.createStream(this.song.url);
await this.joinVC(checkChangeVC);
this.emit(Event.PLAY_SONG, this.song);
if (!this.guild?.id) await this._awaitDM();
this._play();
this._playingTime = Date.now();
}
async previous() {
if (!this._previousSongs.length) throw new Error('No previous songs');
const currentSong = this.song;
// add to queue
this._queue.enqueue(currentSong);
const previousSong = this._previousSongs.pop();
this.song = previousSong;
await this.createStream(this.song.url);
await this.joinVC();
this._play();
this.emit(Event.PLAY_SONG, this.song, this.queue);
}
async joinVC(changeVC = false) {
if (this.currentConnection && !changeVC) {
this.currentConnection.subscribe(this._player);
} else if (this.channel instanceof Discord.VoiceChannel) {
const connection = DjsVoice.joinVoiceChannel({
channelId: this.channel.id,
guildId: this.guild.id,
adapterCreator: this.guild.voiceAdapterCreator,
});
await DjsVoice.entersState(
connection,
DjsVoice.VoiceConnectionStatus.Ready,
10_000,
);
connection.subscribe(this._player);
} else if (this.channel instanceof Discord.StageChannel) {
const connection = DjsVoice.joinVoiceChannel({
channelId: this.channel.id,
guildId: this.guild.id,
adapterCreator: this.guild.voiceAdapterCreator,
});
await DjsVoice.entersState(
connection,
DjsVoice.VoiceConnectionStatus.Ready,
10_000,
);
connection.subscribe(this._player);
await this.channel.guild.members.me.voice
.setSuppressed(false)
.catch(async () => {
return await this.channel.guild.members.me.voice
.setRequestToSpeak(true);
});
} else {
const connection = this.channel.voiceConnection || await this.channel.call();
connection.subscribe(this._player);
}
}
_discordEvent() {
// Event sus .-.
this.client.on('voiceStateUpdate', (oldState, newState) => {
if (!this._currentResourceAudio) return;
if (newState.guild?.id == this.guild?.id) {
if (oldState.channel?.id !== newState.channel?.id && oldState.channel?.id && newState.channel?.id && newState.id == this.client.user.id) {
// change vc
}
if (newState.id == this.client.user.id && oldState.channel?.members?.has(this.client.user.id) && !newState.channel?.members?.has(this.client.user.id)) {
this._stop();
this.emit(Event.LEAVE_VC, this.message);
}
if (newState.channel?.members?.has(this.client.user.id) && !newState.channel?.members?.filter(m => m.id != this.client.user.id && !m.bot).size) {
// empty
if (this.options.leaveOnEmpty && !this._timeoutEmpty) {
this._timeoutEmpty = setTimeout(() => {
this._stop(false, true);
this.emit(Event.EMPTY, this.message);
}, this.options.emptyCooldown);
}
}
if (newState.channel?.members?.has(this.client.user.id) && newState.channel?.members?.filter(m => m.id != this.client.user.id && !m.bot).size > 0) {
// not empty
if (this._timeoutEmpty) clearTimeout(this._timeoutEmpty);
this._timeoutEmpty = undefined;
}
} else if (!this.guild?.id && !newState.guild?.id) {
// DM channels
if (!newState.channel?.voiceUsers?.filter(m => m.id != this.client.user.id).size) {
// empty
if (this.options.leaveOnEmpty && !this._timeoutEmpty) {
this._timeoutEmpty = setTimeout(() => {
this._stop(false, true);
this.emit(Event.EMPTY, this.message);
}, this.options.emptyCooldown);
}
}
if (newState.channel?.voiceUsers?.filter(m => m.id != this.client.user.id).size > 0) {
// not empty
if (this._timeoutEmpty) clearTimeout(this._timeoutEmpty);
this._timeoutEmpty = undefined;
}
}
});
}
_awaitDM () {
if (!this.options.waitingUserToPlayInDMs) return true;
return new Promise(resolve => {
let i = setInterval(() => {
const m = this.channel.voiceUsers.get(this.client.user.id);
if (m) {
clearInterval(i);
resolve(true);
}
}, 250);
})
}
_privateEvent() {
this.on('next_song', async () => {
await this._skip().catch(() => {
if (this.message) this.emit(Event.FINISH, this.message);
this._reset();
});
});
}
_playerEvent() {
const player = this._player;
player.on('stateChange', async (oldState, newState) => {
// idle -> playing
// idle -> buffering
// buffering -> playing
// playing -> idle
if (newState.status.toLowerCase() == 'idle') {
this.isPlaying = false;
} else if (newState.status.toLowerCase() == 'paused' || newState.status.toLowerCase() == 'autopaused') {
this.isPlaying = false;
} else {
this.isPlaying = true;
}
this._currentTime = newState.playbackDuration;
//
if (oldState.status == 'playing' && newState.status == 'idle') {
this.emit('next_song');
}
});
player.on('error', (e) => {
this.emit(Event.ERROR, e);
});
}
}
module.exports = Player;
/* Example
const player = new Player(client, options);
player
.on('playSong', song => {
player.message.channel.send(`Now playing: ${song.title}`);
})
.on('addSong', song => {
player.message.channel.send(`Added: ${song.title}`);
})
.on('addPlaylist', playlist => {
player.message.channel.send(`Added Playlist: ${playlist.title}`);
})
.on('disconnect', (message) => {
message.channel.send('Disconnected from voice channel.');
})
.on('finish', (message) => {
message.channel.send('Finished playing.');
})
.on('empty', (message) => {
message.channel.send('The queue is empty.');
})
.on('error', error => {
console.log('Music error', error);
})
client.player = player;
// Method
client.player.play({
message,
channel: message.member.voice.channel, // VoiceChannel | DMChannel | StageChannel | GroupDMChannel
query: string,
});
client.player.skip();
client.player.previous();
client.player.pause();
client.player.resume();
client.player.setVolume(50); // 50%
client.player.setLoopMode(1); // 0: none, 1: song, 2: queue;
client.player.stop();
// Options
options = {
directLink: true, // Whether or not play direct link of the song (not support)
joinNewVoiceChannel: true, // Whether or not joining the new voice channel when using #play method
waitingUserToPlayInDMs: true, // Waiting User join Call to play in DM channels
nsfw: false, // Whether or not play NSFW
leaveOnEmpty: true, // Whether or not leave voice channel when empty (not working)
leaveOnFinish: true, // Whether or not leave voice channel when finish
leaveOnStop: true, // Whether or not leave voice channel when stop
savePreviousSongs: true, // Whether or not save previous songs
emptyCooldown: 10_000, // Cooldown when empty voice channel
}
// Properties
song = Song;
guild = Discord.Guild;
channel = Channel;
client = Discord.Client;
isPlaying = false;
volume = 100;
currentTime = Unix timestamp miliseconds;
currentConnection = VoiceConnection;
queue: Song[];
previousSongs: Song[];
loopMode = 0;
*/
```