Skip to content

Commit

Permalink
add: Introduce new sharding system (#149)
Browse files Browse the repository at this point in the history
* add: base for /blacklist command (#101)

This commit will add the base for /blacklist command with raw string and not translated. Also this commit is not tested

* merge: latest update from main branch (#146)

* improve: logging system interface

* add: host option to bind into 0.0.0.0 ip address

* add: host option to config file

* add: spanish lang to ByteBlaze

* add: blacklist cmmand; merge: all filter command

This commit fill merge all filter command to 1 file to optimize and reduce code. Also this command introduce brand new command named blacklist to block user/guild rom using bot

* remove: file command due to lavalink restriction

* fix: 24/7 rejoin bug (#137)

* add: MAX Length for a song command (user wide, some command maynot work, #86)

* add: rewritten sharding system

* fix: sometimes bot leave server suddenly
  • Loading branch information
RainyXeon authored Aug 23, 2024
1 parent 0052c26 commit 985d1df
Show file tree
Hide file tree
Showing 22 changed files with 170 additions and 88 deletions.
5 changes: 5 additions & 0 deletions example.full.app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,11 @@ utilities:
whitelist: [] # Example: ["lavalink.dev"]
auth: 'youshallnotpass'

# Because discord-hybrid-sharding not working with this bot
SHARDING_SYSTEM:
shardsPerClusters: 2
totalClusters: 2

# You can custom your emoji here!
emojis:
PLAYER:
Expand Down
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"build:languages": "node --no-deprecation ./scripts/copyLanguagePackage.mjs",
"build:manifest": "node --no-deprecation ./scripts/copyManifest.mjs",
"start": "node --no-deprecation ./dist/index.js",
"start:shard": "node --no-deprecation ./dist/shard/index.js",
"start:shard": "node --no-deprecation ./dist/cluster/index.js",
"dev": "npx nodemon ./src/index.ts",
"build:prettier": "npx prettier -w ./src",
"start:pm2": "npx pm2-runtime start ecosystem.config.cjs --env production"
Expand Down Expand Up @@ -48,7 +48,6 @@
"chalk": "^5.3.0",
"chillout": "^5.0.0",
"common-tags": "^1.8.2",
"discord-hybrid-sharding": "^2.1.4",
"discord.js": "^14.14.1",
"dreamvast.quick.db": "10.0.0-unsupported",
"fast-xml-parser": "^4.3.6",
Expand Down
2 changes: 2 additions & 0 deletions src/@types/Config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ClusterManagerOptions } from '../cluster/core.js'
import { RainlinkNodeOptions } from '../rainlink/main.js'

export interface Config {
Expand All @@ -17,6 +18,7 @@ export interface Bot {
}

export interface Utilities {
SHARDING_SYSTEM: ClusterManagerOptions
AUTO_RESUME: boolean
DELETE_MSG_TIMEOUT: number
DATABASE: Database
Expand Down
27 changes: 27 additions & 0 deletions src/cluster/bot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Manager } from '../manager.js'
import { ConfigDataService } from '../services/ConfigDataService.js'
import { ClusterManager } from './core.js'

export function bootBot(clusterManager?: ClusterManager) {
const configData = new ConfigDataService().data
const byteblaze = new Manager(
configData,
configData.utilities.MESSAGE_CONTENT.enable,
clusterManager
)

// Anti crash handling
process
.on('unhandledRejection', (error) => byteblaze.logger.unhandled('AntiCrash', error))
.on('uncaughtException', (error) => byteblaze.logger.unhandled('AntiCrash', error))
.on('uncaughtExceptionMonitor', (error) => byteblaze.logger.unhandled('AntiCrash', error))
.on('exit', () =>
byteblaze.logger.info('ClientManager', `Successfully Powered Off ByteBlaze, Good Bye!`)
)
.on('SIGINT', () => {
byteblaze.logger.info('ClientManager', `Powering Down ByteBlaze...`)
process.exit(0)
})

byteblaze.start()
}
63 changes: 63 additions & 0 deletions src/cluster/core.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import cluster from 'node:cluster'
import process from 'node:process'
import { config } from 'dotenv'
import { bootBot } from './bot.js'
config()

export interface ClusterManagerOptions {
shardsPerClusters: number
totalClusters: number
}

export class ClusterManager {
public readonly clusterShardList: Record<string, number[]> = {}
public readonly totalShards: number = 0
constructor(public readonly options: ClusterManagerOptions) {
this.totalShards = this.options.totalClusters * this.options.shardsPerClusters
const shardArrayID = this.arrayRange(0, this.totalShards - 1, 1)
this.arrayChunk<number>(shardArrayID, this.options.shardsPerClusters).map((value, index) => {
this.clusterShardList[String(index + 1)] = value
})
}

public async start() {
if (cluster.isPrimary) {
this.log('INFO', `Primary process ${process.pid} is running`)
for (let i = 0; i < this.options.totalClusters; i++) {
cluster.fork()
}

cluster.on('exit', (worker) => {
this.log('WARN', `worker ${worker.process.pid} / ${worker.id} died`)
})
} else {
bootBot(this)

this.log('INFO', `Worker ${process.pid} / ${cluster.worker.id} started`)
}
}

public getShard(clusterId: number) {
return this.clusterShardList[String(clusterId)]
}

protected arrayRange(start: number, stop: number, step: number) {
return Array.from({ length: (stop - start) / step + 1 }, (_, index) => start + index * step)
}

protected arrayChunk<D = unknown>(array: D[], chunkSize: number): D[][] {
return [].concat.apply(
[],
array.map(function (_, i) {
return i % chunkSize ? [] : [array.slice(i, i + chunkSize)]
})
)
}

protected log(level: string, msg: string, pad: number = 9) {
const date = new Date(Date.now()).toISOString()
const prettyLevel = level.toUpperCase().padEnd(pad)
const prettyClass = 'ClusterManager'.padEnd(28)
console.log(`${date} - ${prettyLevel} - ${prettyClass} - ${msg}`)
}
}
10 changes: 10 additions & 0 deletions src/cluster/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { ConfigDataService } from '../services/ConfigDataService.js'
import { ClusterManager } from './core.js'

const configData = new ConfigDataService().data

configData.utilities.SHARDING_SYSTEM

const manager = new ClusterManager(configData.utilities.SHARDING_SYSTEM)

manager.start()
1 change: 0 additions & 1 deletion src/commands/Image/Avatar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ export default class implements Command {
await handler.deferReply()
const data = handler.args[0]
const getData = await handler.parseMentions(data)
console.log(data, getData)

if (data && getData && getData.type !== ParseMentionEnum.USER)
return handler.editReply({
Expand Down
7 changes: 5 additions & 2 deletions src/commands/Music/Play.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,9 @@ export default class implements Command {
player.textId = handler.channel!.id

const result = await player.search(value, { requester: handler.user })
const tracks = result.tracks.filter(e => typeof maxLength !== 'string' ? e.duration > maxLength : e)
const tracks = result.tracks.filter((e) =>
typeof maxLength !== 'string' ? e.duration > maxLength : e
)

if (!result.tracks.length)
return handler.editReply({
Expand Down Expand Up @@ -174,6 +176,7 @@ export default class implements Command {
return `[${tracks[0].title}](${value})`
} else {
return `[${tracks[0].title}](${tracks[0].uri})`
return `[${tracks[0].title}](${tracks[0].uri})`
}
}
}
Expand Down Expand Up @@ -209,7 +212,7 @@ export default class implements Command {
}
const searchRes = await client.rainlink.search(url || Random)

const tracks = searchRes.tracks.filter(e => maxLength ? e.duration > maxLength : e)
const tracks = searchRes.tracks.filter((e) => (maxLength ? e.duration > maxLength : e))

if (tracks.length == 0 || !searchRes.tracks) {
return choice.push({ name: 'Error song not matches', value: url })
Expand Down
7 changes: 4 additions & 3 deletions src/commands/Playlist/Add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export default class implements Command {
const result = await client.rainlink.search(input, {
requester: handler.user,
})
const tracks = result.tracks.filter(e => maxLength ? e.duration > maxLength : e)
const tracks = result.tracks.filter((e) => (maxLength ? e.duration > maxLength : e))

if (!result.tracks.length)
return handler.editReply({
Expand Down Expand Up @@ -248,7 +248,9 @@ export default class implements Command {
}
const searchRes = await client.rainlink.search(url || Random)

const tracks = searchRes.tracks.filter(e => typeof maxLength !== 'string' ? e.duration > maxLength : e)
const tracks = searchRes.tracks.filter((e) =>
typeof maxLength !== 'string' ? e.duration > maxLength : e
)

if (tracks.length == 0 || !searchRes.tracks) {
return choice.push({ name: 'Error song not matches', value: url })
Expand All @@ -262,7 +264,6 @@ export default class implements Command {
})
}


await (interaction as AutocompleteInteraction).respond(choice).catch(() => {})
}
}
11 changes: 6 additions & 5 deletions src/commands/Utils/MaxLength.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export default class implements Command {
let value: number
const time = handler.args[0]

if (time == "none" || time == "0:00") {
if (time == 'none' || time == '0:00') {
await client.db.maxlength.delete(handler.user.id)
return handler.editReply({
embeds: [
Expand Down Expand Up @@ -61,14 +61,15 @@ export default class implements Command {

const player = client.rainlink.players.get(handler.guild!.id) as RainlinkPlayer

if (player && player.queue.length !== 0) player.queue.forEach((track, trackIndex) => {
if (track.duration >= value) player.queue.remove(trackIndex)
})
if (player && player.queue.length !== 0)
player.queue.forEach((track, trackIndex) => {
if (track.duration >= value) player.queue.remove(trackIndex)
})

await client.db.maxlength.set(handler.user.id, value)

const embed = new EmbedBuilder()
.setDescription(client.i18n.get(handler.language, "command.utils", "ml_set", { time } ))
.setDescription(client.i18n.get(handler.language, 'command.utils', 'ml_set', { time }))
.setColor(client.color)
return handler.editReply({ embeds: [embed] })
}
Expand Down
3 changes: 2 additions & 1 deletion src/database/keyChecker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Manager } from '../manager.js'
import { KeyCheckerEnum } from '../@types/KeyChecker.js'
import { LoggerService } from '../services/LoggerService.js'
import utils from 'node:util'
import cluster from 'node:cluster'

export class keyChecker {
obj: Record<string, any>
Expand All @@ -20,7 +21,7 @@ export class keyChecker {
}

execute() {
const logger = new LoggerService(this.client)
const logger = new LoggerService(this.client, cluster.worker.id)
const objReqKey = Object.keys(this.sampleConfig)
const res = this.checkEngine()

Expand Down
2 changes: 1 addition & 1 deletion src/database/schema/MaxLength.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export type MaxLength = number
export type MaxLength = number
2 changes: 1 addition & 1 deletion src/database/table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export class TableSetup {
songNoti: await baseDB.table<SongNoti>('songNoti'),
preGuild: await baseDB.table<Premium>('preGuild'),
blacklist: await baseDB.table<Blacklist>('blacklist'),
maxlength: await baseDB.table<MaxLength>('maxlength')
maxlength: await baseDB.table<MaxLength>('maxlength'),
}

this.client.isDatabaseConnected = true
Expand Down
2 changes: 1 addition & 1 deletion src/handlers/Player/loadContent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ export class PlayerContentLoader {
const maxLength = await client.db.maxlength.get(message.author.id)

const result = await player.search(song, { requester: message.author })
const tracks = result.tracks.filter(e => maxLength ? e.duration > maxLength : e)
const tracks = result.tracks.filter((e) => (maxLength ? e.duration > maxLength : e))

if (!result.tracks.length) {
msg
Expand Down
23 changes: 15 additions & 8 deletions src/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import {
import { DatabaseService } from './database/index.js'
import { resolve } from 'path'
import { LoggerService } from './services/LoggerService.js'
import { ClusterClient, getInfo } from 'discord-hybrid-sharding'
import { join, dirname } from 'path'
import { fileURLToPath } from 'url'
import { WebServer } from './web/server.js'
Expand All @@ -35,8 +34,18 @@ import { RainlinkFilterData, RainlinkPlayer } from './rainlink/main.js'
import { TopggService } from './services/TopggService.js'
import { Collection } from './structures/Collection.js'
import { Localization } from './structures/Localization.js'
import { ClusterManager } from './cluster/core.js'
import cluster from 'node:cluster'
config()

function getShard(clusterManager: ClusterManager) {
const shardListData = clusterManager.getShard(cluster.worker.id)
return {
shards: shardListData,
shardCount: clusterManager.totalShards,
}
}

export class Manager extends Client {
public metadata: Metadata
public logger: LoggerService
Expand Down Expand Up @@ -70,17 +79,17 @@ export class Manager extends Client {
public enSwitchMod!: ActionRowBuilder<ButtonBuilder>
public topgg?: TopggService
public icons: Emojis
public cluster?: ClusterClient<Client>
public REGEX: RegExp[]
public selectMenuOptions: StringSelectMenuOptionBuilder[] = []

constructor(
public config: Config,
isMsgEnable: boolean
isMsgEnable: boolean,
public clusterManager?: ClusterManager
) {
super({
shards: process.env.IS_SHARING == 'true' ? getInfo().SHARD_LIST : 'auto',
shardCount: process.env.IS_SHARING == 'true' ? getInfo().TOTAL_SHARDS : 1,
shards: clusterManager ? getShard(clusterManager).shards : 'auto',
shardCount: clusterManager ? getShard(clusterManager).shardCount : 1,
allowedMentions: {
parse: ['roles', 'users', 'everyone'],
repliedUser: false,
Expand All @@ -101,7 +110,7 @@ export class Manager extends Client {

// Initial basic bot config
const __dirname = dirname(fileURLToPath(import.meta.url))
this.logger = new LoggerService(this)
this.logger = new LoggerService(this, cluster.worker.id)
this.metadata = new ManifestService().data.metadata.bot
this.owner = this.config.bot.OWNER_ID
this.color = (this.config.bot.EMBED_COLOR || '#2b2d31') as ColorResolvable
Expand Down Expand Up @@ -146,8 +155,6 @@ export class Manager extends Client {
// Icons setup
this.icons = this.config.emojis

this.cluster = process.env.IS_SHARING == 'true' ? new ClusterClient(this) : undefined

this.rainlink = new RainlinkInit(this).init
for (const key of Object.keys(RainlinkFilterData)) {
const firstUpperCase = key.charAt(0).toUpperCase() + key.slice(1)
Expand Down
4 changes: 2 additions & 2 deletions src/rainlink/Plugin/Spotify/Plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ export class RainlinkPlugin extends SourceRainlinkPlugin {

while (
next &&
(!this.options.playlistPageLimit ? true : (page < this.options.playlistPageLimit ?? 1))
(!this.options.playlistPageLimit ? true : page < this.options.playlistPageLimit ?? 1)
) {
const nextTracks = await this.requestManager.makeRequest<PlaylistTracks>(next ?? '', true)
page++
Expand Down Expand Up @@ -259,7 +259,7 @@ export class RainlinkPlugin extends SourceRainlinkPlugin {
let page = 1
while (
next &&
(!this.options.playlistPageLimit ? true : (page < this.options.playlistPageLimit ?? 1))
(!this.options.playlistPageLimit ? true : page < this.options.playlistPageLimit ?? 1)
) {
const nextTracks = await this.requestManager.makeRequest<PlaylistTracks>(next ?? '', true)
page++
Expand Down
2 changes: 1 addition & 1 deletion src/rainlink/Rainlink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -576,7 +576,7 @@ export class Rainlink extends EventEmitter {
async search(query: string, options?: RainlinkSearchOptions): Promise<RainlinkSearchResult> {
const node =
options && options?.nodeName
? (this.nodes.get(options.nodeName) ?? (await this.nodes.getLeastUsed()))
? this.nodes.get(options.nodeName) ?? (await this.nodes.getLeastUsed())
: await this.nodes.getLeastUsed()

if (!node) throw new Error('No node is available')
Expand Down
4 changes: 4 additions & 0 deletions src/services/ConfigDataService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,10 @@ export class ConfigDataService {
AUTO_RESUME: false,
TOPGG_TOKEN: '',
DELETE_MSG_TIMEOUT: 2000,
SHARDING_SYSTEM: {
shardsPerClusters: 2,
totalClusters: 2,
},
DATABASE: {
driver: 'json',
config: { path: './cylane.database.json' },
Expand Down
Loading

0 comments on commit 985d1df

Please sign in to comment.