shoukaku-bun 4.2.0-b
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +88 -0
- package/index.ts +9 -0
- package/package.json +56 -0
- package/src/Constants.ts +55 -0
- package/src/Shoukaku.ts +295 -0
- package/src/Utils.ts +58 -0
- package/src/connectors/Connector.ts +49 -0
- package/src/connectors/README.md +42 -0
- package/src/connectors/libs/DiscordJS.ts +21 -0
- package/src/connectors/libs/Eris.ts +21 -0
- package/src/connectors/libs/OceanicJS.ts +21 -0
- package/src/connectors/libs/Seyfert.ts +26 -0
- package/src/connectors/libs/index.ts +4 -0
- package/src/guild/Connection.ts +248 -0
- package/src/guild/Player.ts +543 -0
- package/src/node/Node.ts +442 -0
- package/src/node/Rest.ts +433 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Luigi Colantuono
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
## Shoukaku-Bun
|
|
2
|
+
|
|
3
|
+
> Powerfull, Lightweight wrapper around Lavalink
|
|
4
|
+
|
|
5
|
+
[](https://discordapp.com/invite/FVqbtGu)
|
|
6
|
+
[](https://www.npmjs.com/package/shoukaku)
|
|
7
|
+

|
|
8
|
+

|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
<img src="https://azurlane.netojuu.com/images/thumb/d/dc/ShoukakuWeddingWithoutBG.png/767px-ShoukakuWeddingWithoutBG.png">
|
|
13
|
+
</p>
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
- **Bun-Native**: Re-engineered to run exclusively on Bun.Purged all Node.js legacy dependencies (like `ws`).
|
|
18
|
+
- **Zero Latency**: Uses Bun's kernel-level WebSocket for maximum throughput.
|
|
19
|
+
- **Ultra Lightweight**: Optimized for minimal memory footprint (production tested at ~33MB).
|
|
20
|
+
- **TypeScript Native**: No build step required. Direct execution from source.
|
|
21
|
+
- **Stable & Updated**: Based on the rock-solid Shoukaku v4.2.0 logic.
|
|
22
|
+
- **Very cute (Very Important)**
|
|
23
|
+
|
|
24
|
+
### Documentation
|
|
25
|
+
|
|
26
|
+
> https://guide.shoukaku.shipgirl.moe/
|
|
27
|
+
|
|
28
|
+
### Installation
|
|
29
|
+
|
|
30
|
+
This is a specialized fork. Install it directly from GitHub:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
bun add github:LuigiColantuono/shoukaku-bun
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Documentation
|
|
37
|
+
|
|
38
|
+
> https://guide.shoukaku.shipgirl.moe/
|
|
39
|
+
|
|
40
|
+
### Getting Started
|
|
41
|
+
|
|
42
|
+
> https://guide.shoukaku.shipgirl.moe/guides/1-getting-started/
|
|
43
|
+
|
|
44
|
+
### Supported Libraries
|
|
45
|
+
|
|
46
|
+
> https://guide.shoukaku.shipgirl.moe/guides/5-connectors/
|
|
47
|
+
|
|
48
|
+
### Example Bot
|
|
49
|
+
|
|
50
|
+
> https://github.com/Deivu/Kongou
|
|
51
|
+
|
|
52
|
+
### Configuration Options
|
|
53
|
+
|
|
54
|
+
```js
|
|
55
|
+
// Parameters for main class init, Options is the Configuration Options
|
|
56
|
+
new Shoukaku(new Connectors.DiscordJS(client), Nodes, Options);
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
| Option | Type | Default | Description | Notes |
|
|
60
|
+
| ---------------------- | ---------------------- | -------- | ------------------------------------------------------------------------------------------------ | ------------------------ |
|
|
61
|
+
| resume | boolean | false | If you want to enable resuming when your connection to lavalink disconnects | |
|
|
62
|
+
| resumeTimeout | number | 30 | Timeout before lavalink destroys the players on a disconnect | In seconds |
|
|
63
|
+
| resumeByLibrary | boolean | false | If you want to force resume players no matter what even if it's not resumable by lavalink | |
|
|
64
|
+
| reconnectTries | number | 3 | Number of tries to reconnect to lavalink before disconnecting | |
|
|
65
|
+
| reconnectInterval | number | 5 | Timeout between reconnects | In seconds |
|
|
66
|
+
| restTimeout | number | 60 | Maximum amount of time to wait for rest lavalink api requests | In seconds |
|
|
67
|
+
| moveOnDisconnect | boolean | false | Whether to move players to a different lavalink node when a node disconnects | |
|
|
68
|
+
| userAgent | string | (auto) | Changes the user-agent used for lavalink requests | Not recommeded to change |
|
|
69
|
+
| structures | Object{rest?, player?} | {} | Custom structures for shoukaku to use | |
|
|
70
|
+
| voiceConnectionTimeout | number | 15 | Maximum amount of time to wait for a join voice channel command | In seconds |
|
|
71
|
+
| nodeResolver | function | function | Custom node resolver if you want to have your own method of getting the ideal node | |
|
|
72
|
+
|
|
73
|
+
### Wrappers
|
|
74
|
+
|
|
75
|
+
| Name | Link | Description |
|
|
76
|
+
| -------- | --------------------------------------------- | -------------------------------------------------------- |
|
|
77
|
+
| Kazagumo | [Github](https://github.com/Takiyo0/Kazagumo) | A wrapper for Shoukaku that has an internal queue system |
|
|
78
|
+
|
|
79
|
+
> Open a pr if you want to add a wrapper here
|
|
80
|
+
|
|
81
|
+
### Other Links
|
|
82
|
+
|
|
83
|
+
- [Discord](https://discord.gg/XqJw52d35R)
|
|
84
|
+
|
|
85
|
+
- [Lavalink](https://github.com/lavalink-devs/Lavalink)
|
|
86
|
+
|
|
87
|
+
### Code made with ❤ by @ichimakase (Saya) & Luigi
|
|
88
|
+
|
package/index.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * as Connectors from './src/connectors/libs';
|
|
2
|
+
export * as Constants from './src/Constants';
|
|
3
|
+
export * as Utils from './src/Utils';
|
|
4
|
+
export * from './src/connectors/Connector';
|
|
5
|
+
export * from './src/guild/Connection';
|
|
6
|
+
export * from './src/guild/Player';
|
|
7
|
+
export * from './src/node/Node';
|
|
8
|
+
export * from './src/node/Rest';
|
|
9
|
+
export * from './src/Shoukaku';
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "shoukaku-bun",
|
|
3
|
+
"version": "4.2.0-b",
|
|
4
|
+
"description": "Bun-native high-performance fork of Shoukaku. Node-dependencies purged.",
|
|
5
|
+
"main": "index.ts",
|
|
6
|
+
"module": "index.ts",
|
|
7
|
+
"types": "index.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"src",
|
|
10
|
+
"index.ts"
|
|
11
|
+
],
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./index.ts",
|
|
15
|
+
"import": "./index.ts",
|
|
16
|
+
"require": "./index.ts"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"lint": "eslint .",
|
|
21
|
+
"test": "bun test"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"bot",
|
|
25
|
+
"music",
|
|
26
|
+
"lavalink",
|
|
27
|
+
"bun",
|
|
28
|
+
"high-performance",
|
|
29
|
+
"discord"
|
|
30
|
+
],
|
|
31
|
+
"engines": {
|
|
32
|
+
"bun": "1.3.6"
|
|
33
|
+
},
|
|
34
|
+
"author": "Saya",
|
|
35
|
+
"contributors": [
|
|
36
|
+
{
|
|
37
|
+
"name": "Luigi Colantuono",
|
|
38
|
+
"url": "https://github.com/LuigiColantuono",
|
|
39
|
+
"info": "Bun Refactor & Optimization"
|
|
40
|
+
}
|
|
41
|
+
],
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"homepage": "https://github.com/LuigiColantuono/shoukaku-bun",
|
|
44
|
+
"repository": {
|
|
45
|
+
"type": "git",
|
|
46
|
+
"url": "git+https://github.com/LuigiColantuono/shoukaku-bun.git"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@shipgirl/eslint-config": "^0.2.2",
|
|
51
|
+
"@types/bun": "latest",
|
|
52
|
+
"eslint": "^9.39.2",
|
|
53
|
+
"typedoc": "^0.28.16",
|
|
54
|
+
"typescript": "^5.9.3"
|
|
55
|
+
}
|
|
56
|
+
}
|
package/src/Constants.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import Info from '../package.json';
|
|
2
|
+
import type { NodeOption, ShoukakuOptions } from './Shoukaku';
|
|
3
|
+
|
|
4
|
+
export enum State {
|
|
5
|
+
CONNECTING,
|
|
6
|
+
CONNECTED,
|
|
7
|
+
DISCONNECTING,
|
|
8
|
+
DISCONNECTED
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export enum VoiceState {
|
|
12
|
+
SESSION_READY,
|
|
13
|
+
SESSION_ID_MISSING,
|
|
14
|
+
SESSION_ENDPOINT_MISSING,
|
|
15
|
+
SESSION_FAILED_UPDATE
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export enum OpCodes {
|
|
19
|
+
PLAYER_UPDATE = 'playerUpdate',
|
|
20
|
+
STATS = 'stats',
|
|
21
|
+
EVENT = 'event',
|
|
22
|
+
READY = 'ready'
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const Versions = {
|
|
26
|
+
REST_VERSION: 4,
|
|
27
|
+
WEBSOCKET_VERSION: 4
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const ShoukakuDefaults: Required<ShoukakuOptions> = {
|
|
31
|
+
resume: false,
|
|
32
|
+
resumeTimeout: 30,
|
|
33
|
+
resumeByLibrary: false,
|
|
34
|
+
reconnectTries: 3,
|
|
35
|
+
reconnectInterval: 5,
|
|
36
|
+
restTimeout: 60,
|
|
37
|
+
moveOnDisconnect: false,
|
|
38
|
+
userAgent: 'Discord Bot/unknown (https://github.com/shipgirlproject/Shoukaku.git)',
|
|
39
|
+
structures: {},
|
|
40
|
+
voiceConnectionTimeout: 15,
|
|
41
|
+
nodeResolver: (nodes) => [ ...nodes.values() ]
|
|
42
|
+
.filter(node => node.state === State.CONNECTED)
|
|
43
|
+
.sort((a, b) => a.penalties - b.penalties)
|
|
44
|
+
.shift()
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const ShoukakuClientInfo = `${Info.name}/${Info.version} (${Info.repository.url})`;
|
|
48
|
+
|
|
49
|
+
export const NodeDefaults: NodeOption = {
|
|
50
|
+
name: 'Default',
|
|
51
|
+
url: '',
|
|
52
|
+
auth: '',
|
|
53
|
+
secure: false,
|
|
54
|
+
group: undefined
|
|
55
|
+
};
|
package/src/Shoukaku.ts
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { ShoukakuDefaults, VoiceState } from './Constants';
|
|
2
|
+
import { Node } from './node/Node';
|
|
3
|
+
import { Connector } from './connectors/Connector';
|
|
4
|
+
import { Constructor, mergeDefault, TypedEventEmitter } from './Utils';
|
|
5
|
+
import { Player } from './guild/Player';
|
|
6
|
+
import { Rest } from './node/Rest';
|
|
7
|
+
import { Connection } from './guild/Connection';
|
|
8
|
+
|
|
9
|
+
export interface Structures {
|
|
10
|
+
/**
|
|
11
|
+
* A custom structure that extends the Rest class
|
|
12
|
+
*/
|
|
13
|
+
rest?: Constructor<Rest>;
|
|
14
|
+
/**
|
|
15
|
+
* A custom structure that extends the Player class
|
|
16
|
+
*/
|
|
17
|
+
player?: Constructor<Player>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface NodeOption {
|
|
21
|
+
/**
|
|
22
|
+
* Name of the Lavalink node
|
|
23
|
+
*/
|
|
24
|
+
name: string;
|
|
25
|
+
/**
|
|
26
|
+
* Lavalink node host and port without any prefix
|
|
27
|
+
*/
|
|
28
|
+
url: string;
|
|
29
|
+
/**
|
|
30
|
+
* Credentials to access Lavalink
|
|
31
|
+
*/
|
|
32
|
+
auth: string;
|
|
33
|
+
/**
|
|
34
|
+
* Whether to use secure protocols or not
|
|
35
|
+
*/
|
|
36
|
+
secure?: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Name of the Lavalink node group
|
|
39
|
+
*/
|
|
40
|
+
group?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ShoukakuOptions {
|
|
44
|
+
/**
|
|
45
|
+
* Whether to resume a connection on disconnect to Lavalink (Server Side) (Note: DOES NOT RESUME WHEN THE LAVALINK SERVER DIES)
|
|
46
|
+
*/
|
|
47
|
+
resume?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Time to wait before lavalink starts to destroy the players of the disconnected client
|
|
50
|
+
*/
|
|
51
|
+
resumeTimeout?: number;
|
|
52
|
+
/**
|
|
53
|
+
* Whether to resume the players by doing it in the library side (Client Side) (Note: TRIES TO RESUME REGARDLESS OF WHAT HAPPENED ON A LAVALINK SERVER)
|
|
54
|
+
*/
|
|
55
|
+
resumeByLibrary?: boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Number of times to try and reconnect to Lavalink before giving up
|
|
58
|
+
*/
|
|
59
|
+
reconnectTries?: number;
|
|
60
|
+
/**
|
|
61
|
+
* Timeout before trying to reconnect
|
|
62
|
+
*/
|
|
63
|
+
reconnectInterval?: number;
|
|
64
|
+
/**
|
|
65
|
+
* Time to wait for a response from the Lavalink REST API before giving up
|
|
66
|
+
*/
|
|
67
|
+
restTimeout?: number;
|
|
68
|
+
/**
|
|
69
|
+
* Whether to move players to a different Lavalink node when a node disconnects
|
|
70
|
+
*/
|
|
71
|
+
moveOnDisconnect?: boolean;
|
|
72
|
+
/**
|
|
73
|
+
* User Agent to use when making requests to Lavalink
|
|
74
|
+
*/
|
|
75
|
+
userAgent?: string;
|
|
76
|
+
/**
|
|
77
|
+
* Custom structures for shoukaku to use
|
|
78
|
+
*/
|
|
79
|
+
structures?: Structures;
|
|
80
|
+
/**
|
|
81
|
+
* Timeout before abort connection
|
|
82
|
+
*/
|
|
83
|
+
voiceConnectionTimeout?: number;
|
|
84
|
+
/**
|
|
85
|
+
* Node Resolver to use if you want to customize it
|
|
86
|
+
*/
|
|
87
|
+
nodeResolver?: (nodes: Map<string, Node>, connection?: Connection) => Node | undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface VoiceChannelOptions {
|
|
91
|
+
guildId: string;
|
|
92
|
+
shardId: number;
|
|
93
|
+
channelId: string;
|
|
94
|
+
deaf?: boolean;
|
|
95
|
+
mute?: boolean;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Interfaces are not final, but types are, and therefore has an index signature
|
|
99
|
+
// https://stackoverflow.com/a/64970740
|
|
100
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
|
101
|
+
export type ShoukakuEvents = {
|
|
102
|
+
/**
|
|
103
|
+
* Emitted when reconnect tries are occurring and how many tries are left
|
|
104
|
+
* @eventProperty
|
|
105
|
+
*/
|
|
106
|
+
'reconnecting': [name: string, reconnectsLeft: number, reconnectInterval: number];
|
|
107
|
+
/**
|
|
108
|
+
* Emitted when data useful for debugging is produced
|
|
109
|
+
* @eventProperty
|
|
110
|
+
*/
|
|
111
|
+
'debug': [name: string, info: string];
|
|
112
|
+
/**
|
|
113
|
+
* Emitted when an error occurs
|
|
114
|
+
* @eventProperty
|
|
115
|
+
*/
|
|
116
|
+
'error': [name: string, error: Error];
|
|
117
|
+
/**
|
|
118
|
+
* Emitted when Shoukaku is ready to receive operations
|
|
119
|
+
* @eventProperty
|
|
120
|
+
*/
|
|
121
|
+
'ready': [name: string, lavalinkResume: boolean, libraryResume: boolean];
|
|
122
|
+
/**
|
|
123
|
+
* Emitted when a websocket connection to Lavalink closes
|
|
124
|
+
* @eventProperty
|
|
125
|
+
*/
|
|
126
|
+
'close': [name: string, code: number, reason: string];
|
|
127
|
+
/**
|
|
128
|
+
* Emitted when a websocket connection to Lavalink disconnects
|
|
129
|
+
* @eventProperty
|
|
130
|
+
*/
|
|
131
|
+
'disconnect': [name: string, count: number];
|
|
132
|
+
/**
|
|
133
|
+
* Emitted when a raw message is received from Lavalink
|
|
134
|
+
* @eventProperty
|
|
135
|
+
*/
|
|
136
|
+
'raw': [name: string, json: unknown];
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Main Shoukaku class
|
|
141
|
+
*/
|
|
142
|
+
export class Shoukaku extends TypedEventEmitter<ShoukakuEvents> {
|
|
143
|
+
/**
|
|
144
|
+
* Discord library connector
|
|
145
|
+
*/
|
|
146
|
+
public readonly connector: Connector;
|
|
147
|
+
/**
|
|
148
|
+
* Shoukaku options
|
|
149
|
+
*/
|
|
150
|
+
public readonly options: Required<ShoukakuOptions>;
|
|
151
|
+
/**
|
|
152
|
+
* Connected Lavalink nodes
|
|
153
|
+
*/
|
|
154
|
+
public readonly nodes: Map<string, Node>;
|
|
155
|
+
/**
|
|
156
|
+
* Voice connections being handled
|
|
157
|
+
*/
|
|
158
|
+
public readonly connections: Map<string, Connection>;
|
|
159
|
+
/**
|
|
160
|
+
* Players being handled
|
|
161
|
+
*/
|
|
162
|
+
public readonly players: Map<string, Player>;
|
|
163
|
+
/**
|
|
164
|
+
* Shoukaku instance identifier
|
|
165
|
+
*/
|
|
166
|
+
public id: string | null;
|
|
167
|
+
/**
|
|
168
|
+
* @param connector A Discord library connector
|
|
169
|
+
* @param nodes An array that conforms to the NodeOption type that specifies nodes to connect to
|
|
170
|
+
* @param options Options to pass to create this Shoukaku instance
|
|
171
|
+
* @param options.resume Whether to resume a connection on disconnect to Lavalink (Server Side) (Note: DOES NOT RESUME WHEN THE LAVALINK SERVER DIES)
|
|
172
|
+
* @param options.resumeTimeout Time to wait before lavalink starts to destroy the players of the disconnected client
|
|
173
|
+
* @param options.resumeByLibrary Whether to resume the players by doing it in the library side (Client Side) (Note: TRIES TO RESUME REGARDLESS OF WHAT HAPPENED ON A LAVALINK SERVER)
|
|
174
|
+
* @param options.reconnectTries Number of times to try and reconnect to Lavalink before giving up
|
|
175
|
+
* @param options.reconnectInterval Timeout before trying to reconnect
|
|
176
|
+
* @param options.restTimeout Time to wait for a response from the Lavalink REST API before giving up
|
|
177
|
+
* @param options.moveOnDisconnect Whether to move players to a different Lavalink node when a node disconnects
|
|
178
|
+
* @param options.userAgent User Agent to use when making requests to Lavalink
|
|
179
|
+
* @param options.structures Custom structures for shoukaku to use
|
|
180
|
+
* @param options.nodeResolver Used if you have custom lavalink node resolving
|
|
181
|
+
*/
|
|
182
|
+
constructor(connector: Connector, nodes: NodeOption[], options: ShoukakuOptions = {}) {
|
|
183
|
+
super();
|
|
184
|
+
this.connector = connector.set(this);
|
|
185
|
+
this.options = mergeDefault<ShoukakuOptions>(ShoukakuDefaults, options);
|
|
186
|
+
this.nodes = new Map();
|
|
187
|
+
this.connections = new Map();
|
|
188
|
+
this.players = new Map();
|
|
189
|
+
this.id = null;
|
|
190
|
+
this.connector.listen(nodes);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Gets an ideal node based on the nodeResolver you provided
|
|
195
|
+
* @param connection Optional connection class for ideal node selection, if you use it
|
|
196
|
+
* @returns An ideal node for you to do things with
|
|
197
|
+
*/
|
|
198
|
+
public getIdealNode(connection?: Connection): Node | undefined {
|
|
199
|
+
return this.options.nodeResolver(this.nodes, connection);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Add a Lavalink node to the pool of available nodes
|
|
204
|
+
* @param options.name Name of this node
|
|
205
|
+
* @param options.url URL of Lavalink
|
|
206
|
+
* @param options.auth Credentials to access Lavalink
|
|
207
|
+
* @param options.secure Whether to use secure protocols or not
|
|
208
|
+
* @param options.group Group of this node
|
|
209
|
+
*/
|
|
210
|
+
public addNode(options: NodeOption): void {
|
|
211
|
+
const node = new Node(this, options);
|
|
212
|
+
node.on('debug', (...args) => this.emit('debug', node.name, ...args));
|
|
213
|
+
node.on('reconnecting', (...args) => this.emit('reconnecting', node.name, ...args));
|
|
214
|
+
node.on('error', (...args) => this.emit('error', node.name, ...args));
|
|
215
|
+
node.on('close', (...args) => this.emit('close', node.name, ...args));
|
|
216
|
+
node.on('ready', (...args) => this.emit('ready', node.name, ...args));
|
|
217
|
+
node.on('raw', (...args) => this.emit('raw', node.name, ...args));
|
|
218
|
+
node.once('disconnect', () => this.nodes.delete(node.name));
|
|
219
|
+
node.connect().catch((error) => this.emit('error', node.name, error as Error));
|
|
220
|
+
this.nodes.set(node.name, node);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Remove a Lavalink node from the pool of available nodes
|
|
225
|
+
* @param name Name of the node
|
|
226
|
+
* @param reason Reason of removing the node
|
|
227
|
+
*/
|
|
228
|
+
public removeNode(name: string, reason = 'Remove node executed'): void {
|
|
229
|
+
const node = this.nodes.get(name);
|
|
230
|
+
if (!node) throw new Error('The node name you specified doesn\'t exist');
|
|
231
|
+
node.disconnect(1000, reason);
|
|
232
|
+
this.nodes.delete(name);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Joins a voice channel
|
|
237
|
+
* @param options.guildId GuildId in which the ChannelId of the voice channel is located
|
|
238
|
+
* @param options.shardId ShardId to track where this should send on sharded websockets, put 0 if you are unsharded
|
|
239
|
+
* @param options.channelId ChannelId of the voice channel you want to connect to
|
|
240
|
+
* @param options.deaf Optional boolean value to specify whether to deafen or undeafen the current bot user
|
|
241
|
+
* @param options.mute Optional boolean value to specify whether to mute or unmute the current bot user
|
|
242
|
+
* @returns The created player
|
|
243
|
+
*/
|
|
244
|
+
public async joinVoiceChannel(options: VoiceChannelOptions): Promise<Player> {
|
|
245
|
+
if (this.connections.has(options.guildId))
|
|
246
|
+
throw new Error('This guild already have an existing connection');
|
|
247
|
+
const connection = new Connection(this, options);
|
|
248
|
+
this.connections.set(connection.guildId, connection);
|
|
249
|
+
try {
|
|
250
|
+
await connection.connect();
|
|
251
|
+
} catch (error) {
|
|
252
|
+
this.connections.delete(options.guildId);
|
|
253
|
+
throw error;
|
|
254
|
+
}
|
|
255
|
+
try {
|
|
256
|
+
const node = this.getIdealNode(connection);
|
|
257
|
+
if (!node)
|
|
258
|
+
throw new Error('Can\'t find any nodes to connect on');
|
|
259
|
+
const player = this.options.structures.player ? new this.options.structures.player(connection.guildId, node) : new Player(connection.guildId, node);
|
|
260
|
+
const onUpdate = (state: VoiceState) => {
|
|
261
|
+
if (state !== VoiceState.SESSION_READY) return;
|
|
262
|
+
void player.sendServerUpdate(connection);
|
|
263
|
+
};
|
|
264
|
+
await player.sendServerUpdate(connection);
|
|
265
|
+
connection.on('connectionUpdate', onUpdate);
|
|
266
|
+
this.players.set(player.guildId, player);
|
|
267
|
+
return player;
|
|
268
|
+
} catch (error) {
|
|
269
|
+
connection.disconnect();
|
|
270
|
+
this.connections.delete(options.guildId);
|
|
271
|
+
throw error;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Leaves a voice channel
|
|
277
|
+
* @param guildId The id of the guild you want to delete
|
|
278
|
+
* @returns The destroyed / disconnected player or undefined if none
|
|
279
|
+
*/
|
|
280
|
+
public async leaveVoiceChannel(guildId: string): Promise<void> {
|
|
281
|
+
const connection = this.connections.get(guildId);
|
|
282
|
+
if (connection) {
|
|
283
|
+
connection.disconnect();
|
|
284
|
+
this.connections.delete(guildId);
|
|
285
|
+
}
|
|
286
|
+
const player = this.players.get(guildId);
|
|
287
|
+
if (player) {
|
|
288
|
+
try {
|
|
289
|
+
await player.destroy();
|
|
290
|
+
} catch { /* empty */ }
|
|
291
|
+
player.clean();
|
|
292
|
+
this.players.delete(guildId);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
package/src/Utils.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
|
|
3
|
+
// https://stackoverflow.com/a/67244127
|
|
4
|
+
export abstract class TypedEventEmitter<T extends Record<string, unknown[]>> extends EventEmitter {
|
|
5
|
+
protected constructor() {
|
|
6
|
+
super();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
on<K extends Extract<keyof T, string> | symbol>(eventName: K, listener: (...args: T[Extract<K, string>]) => void): this {
|
|
10
|
+
return super.on(eventName, listener);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
once<K extends Extract<keyof T, string> | symbol>(eventName: K, listener: (...args: T[Extract<K, string>]) => void): this {
|
|
14
|
+
return super.once(eventName, listener);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
off<K extends Extract<keyof T, string> | symbol>(eventName: K, listener: (...args: T[Extract<K, string>]) => void): this {
|
|
18
|
+
return super.off(eventName, listener);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
emit<K extends Extract<keyof T, string> | symbol>(eventName: K, ...args: T[Extract<K, string>]): boolean {
|
|
22
|
+
return super.emit(eventName, ...args);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type Constructor<T> = new (...args: unknown[]) => T;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Merge the default options to user input
|
|
30
|
+
* @param def Default options
|
|
31
|
+
* @param given User input
|
|
32
|
+
* @returns Merged options
|
|
33
|
+
*/
|
|
34
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
35
|
+
export function mergeDefault<T extends Record<string, any>>(def: T, given: T): Required<T> {
|
|
36
|
+
if (!given) return def as Required<T>;
|
|
37
|
+
const defaultKeys: (keyof T)[] = Object.keys(def);
|
|
38
|
+
for (const key in given) {
|
|
39
|
+
if (defaultKeys.includes(key)) continue;
|
|
40
|
+
delete given[key];
|
|
41
|
+
}
|
|
42
|
+
for (const key of defaultKeys) {
|
|
43
|
+
if (def[key] === null || (typeof def[key] === 'string' && def[key].length === 0)) {
|
|
44
|
+
if (!given[key]) throw new Error(`${String(key)} was not found from the given options.`);
|
|
45
|
+
}
|
|
46
|
+
given[key] ??= def[key];
|
|
47
|
+
}
|
|
48
|
+
return given as Required<T>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Wait for a specific amount of time (timeout)
|
|
53
|
+
* @param ms Time to wait in milliseconds
|
|
54
|
+
* @returns A promise that resolves in x seconds
|
|
55
|
+
*/
|
|
56
|
+
export function wait(ms: number): Promise<void> {
|
|
57
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
58
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access */
|
|
2
|
+
import { NodeDefaults } from '../Constants';
|
|
3
|
+
import type { ServerUpdate, StateUpdatePartial } from '../guild/Connection';
|
|
4
|
+
import type { NodeOption, Shoukaku } from '../Shoukaku';
|
|
5
|
+
import { mergeDefault } from '../Utils';
|
|
6
|
+
|
|
7
|
+
export interface ConnectorMethods {
|
|
8
|
+
sendPacket: any;
|
|
9
|
+
getId: any;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const AllowedPackets = [ 'VOICE_STATE_UPDATE', 'VOICE_SERVER_UPDATE' ];
|
|
13
|
+
|
|
14
|
+
export abstract class Connector {
|
|
15
|
+
protected readonly client: any;
|
|
16
|
+
protected manager: Shoukaku | null;
|
|
17
|
+
constructor(client: any) {
|
|
18
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
19
|
+
this.client = client;
|
|
20
|
+
this.manager = null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public set(manager: Shoukaku): Connector {
|
|
24
|
+
this.manager = manager;
|
|
25
|
+
return this;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
protected ready(nodes: NodeOption[]): void {
|
|
29
|
+
this.manager!.id = this.getId();
|
|
30
|
+
for (const node of nodes) this.manager!.addNode(mergeDefault(NodeDefaults, node));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
protected raw(packet: any): void {
|
|
34
|
+
if (!AllowedPackets.includes(packet.t as string)) return;
|
|
35
|
+
const guildId = packet.d.guild_id as string;
|
|
36
|
+
const connection = this.manager!.connections.get(guildId);
|
|
37
|
+
if (!connection) return;
|
|
38
|
+
if (packet.t === 'VOICE_SERVER_UPDATE') return connection.setServerUpdate(packet.d as ServerUpdate);
|
|
39
|
+
const userId = packet.d.user_id as string;
|
|
40
|
+
if (userId !== this.manager!.id) return;
|
|
41
|
+
connection.setStateUpdate(packet.d as StateUpdatePartial);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
abstract getId(): string;
|
|
45
|
+
|
|
46
|
+
abstract sendPacket(shardId: number, payload: unknown, important: boolean): void;
|
|
47
|
+
|
|
48
|
+
abstract listen(nodes: NodeOption[]): void;
|
|
49
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
## Supported Libs
|
|
2
|
+
|
|
3
|
+
> [Discord.JS](https://discord.js.org/#/) (v13.x.x & 14.x.x)
|
|
4
|
+
|
|
5
|
+
```js
|
|
6
|
+
const { Shoukaku, Connectors } = require('shoukaku');
|
|
7
|
+
new Shoukaku(new Connectors.DiscordJS(client), servers, options);
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
> [Eris](https://abal.moe/Eris/) (0.15.x / 0.16.x / 0.17.x)
|
|
11
|
+
|
|
12
|
+
```js
|
|
13
|
+
const { Shoukaku, Connectors } = require('shoukaku');
|
|
14
|
+
new Shoukaku(new Connectors.Eris(client), servers, options)
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
> [Oceanic.JS](https://oceanic.ws/) (1.0.x)
|
|
18
|
+
|
|
19
|
+
```js
|
|
20
|
+
const { Shoukaku, Connectors } = require('shoukaku');
|
|
21
|
+
new Shoukaku(new Connectors.OceanicJS(client), servers, options)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
> [Seyfert](https://seyfert-docs.vercel.app/) (0.1.x)
|
|
25
|
+
|
|
26
|
+
```js
|
|
27
|
+
const { Shoukaku, Connectors } = require('shoukaku');
|
|
28
|
+
new Shoukaku(new Connectors.Seyfert(client), servers, options)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
> Implement your own
|
|
33
|
+
|
|
34
|
+
## Implementing your own
|
|
35
|
+
|
|
36
|
+
> Check **DiscordJS.ts** or **Eris.ts** inside libs folder for a detailed explanation on how to support a library
|
|
37
|
+
|
|
38
|
+
> And Submit a PR so other people don't need to do it themselves, yay!
|
|
39
|
+
|
|
40
|
+
## Support
|
|
41
|
+
|
|
42
|
+
For questions on how to do so, just ask at my support server at [HERE](https://discord.gg/FVqbtGu) (#Development)
|