initial commit: fork
This commit is contained in:
15
apps/bot/.sapphirerc.json
Normal file
15
apps/bot/.sapphirerc.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"projectLanguage": "ts",
|
||||
"locations": {
|
||||
"base": "src",
|
||||
"arguments": "arguments",
|
||||
"commands": "commands",
|
||||
"listeners": "listeners",
|
||||
"preconditions": "preconditions",
|
||||
"interaction-handlers": "interaction-handlers"
|
||||
},
|
||||
"customFileTemplates": {
|
||||
"enabled": false,
|
||||
"location": ""
|
||||
}
|
||||
}
|
||||
73
apps/bot/package.json
Normal file
73
apps/bot/package.json
Normal file
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"name": "@master-bot/bot",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "a discord music bot with guild , gifs and misc commands",
|
||||
"author": "Nir Gal",
|
||||
"license": "ISC",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"build": "pnpm with-env tsc",
|
||||
"watch": "tsc --watch",
|
||||
"copy-scripts": "pnpx ncp ./scripts ./dist/",
|
||||
"dev": "pnpm build && pnpm copy-scripts && run-p watch start",
|
||||
"start": "pnpm with-env node dist/index.js",
|
||||
"with-env": "dotenv -e ../../.env --"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=v18.16.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@discordjs/collection": "^1.5.2",
|
||||
"@lavaclient/spotify": "^3.1.0",
|
||||
"@lavalink/encoding": "^0.1.2",
|
||||
"@master-bot/api": "^0.1.0",
|
||||
"@napi-rs/canvas": "^0.1.41",
|
||||
"@prisma/client": "^5.1.1",
|
||||
"@sapphire/decorators": "^6.0.2",
|
||||
"@sapphire/discord.js-utilities": "^7.0.1",
|
||||
"@sapphire/framework": "^4.5.1",
|
||||
"@sapphire/plugin-hmr": "^2.0.1",
|
||||
"@sapphire/time-utilities": "^1.7.10",
|
||||
"@sapphire/utilities": "^3.13.0",
|
||||
"@t3-oss/env-core": "^0.6.0",
|
||||
"@trpc/client": "^10.37.1",
|
||||
"@trpc/server": "^10.37.1",
|
||||
"axios": "^1.4.0",
|
||||
"colorette": "^2.0.20",
|
||||
"discord.js": "^14.12.1",
|
||||
"genius-discord-lyrics": "1.0.5",
|
||||
"google-translate-api-x": "^10.6.7",
|
||||
"ioredis": "^5.3.2",
|
||||
"iso-639-1": "^2.1.15",
|
||||
"lavaclient": "^4.1.1",
|
||||
"metadata-filter": "^1.3.0",
|
||||
"ncp": "^2.0.0",
|
||||
"node-fetch": "^3.3.2",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"string-progressbar": "^1.0.4",
|
||||
"superjson": "1.13.1",
|
||||
"winston": "^3.10.0",
|
||||
"winston-daily-rotate-file": "^4.7.1",
|
||||
"zod": "^3.21.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lavaclient/types": "^2.1.1",
|
||||
"@sapphire/ts-config": "^4.0.1",
|
||||
"@types/ioredis": "^4.28.10",
|
||||
"@types/node": "^20.4.6",
|
||||
"@typescript-eslint/eslint-plugin": "^6.2.1",
|
||||
"@typescript-eslint/parser": "^6.2.1",
|
||||
"dotenv": "^16.3.1",
|
||||
"dotenv-cli": "^7.2.1",
|
||||
"prettier": "^3.0.1",
|
||||
"tslib": "^2.6.1",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"extends": [
|
||||
"@master-bot/eslint-config/base"
|
||||
]
|
||||
}
|
||||
}
|
||||
20
apps/bot/scripts/audio/lmove.lua
Normal file
20
apps/bot/scripts/audio/lmove.lua
Normal file
@@ -0,0 +1,20 @@
|
||||
local KEY = KEYS[1]
|
||||
local FROM = tonumber(ARGV[1])
|
||||
local TO = tonumber(ARGV[2])
|
||||
|
||||
if FROM == nil then return redis.redis_error('origin must be a number') end
|
||||
if TO == nil then return redis.redis_error('destination must be a number') end
|
||||
|
||||
local list = redis.call('lrange', KEY, 0, -1)
|
||||
|
||||
if FROM == TO then return 'OK' end
|
||||
if FROM < 0 then FROM = #list + FROM end
|
||||
if TO < 0 then TO = #list + TO end
|
||||
|
||||
-- provided indexes are 0-based
|
||||
local val = table.remove(list, FROM + 1)
|
||||
table.insert(list, TO + 1, val)
|
||||
|
||||
redis.call('del', KEY)
|
||||
redis.call('rpush', KEY, unpack(list))
|
||||
return 'OK'
|
||||
20
apps/bot/scripts/audio/lremat.lua
Normal file
20
apps/bot/scripts/audio/lremat.lua
Normal file
@@ -0,0 +1,20 @@
|
||||
local KEY = KEYS[1]
|
||||
local INDEX = tonumber(ARGV[1])
|
||||
|
||||
if INDEX == nil then return redis.redis_error('origin must be a number') end
|
||||
|
||||
local list = redis.call('lrange', KEY, 0, -1)
|
||||
|
||||
if INDEX < 0 then INDEX = #list + INDEX end
|
||||
|
||||
-- provided indexes are 0-based
|
||||
table.remove(list, INDEX + 1)
|
||||
|
||||
redis.call('del', KEY)
|
||||
|
||||
-- If there is at least one element, call rpush
|
||||
if (next(list) ~= nil) then
|
||||
redis.call('rpush', KEY, unpack(list))
|
||||
end
|
||||
|
||||
return 'OK'
|
||||
19
apps/bot/scripts/audio/lshuffle.lua
Normal file
19
apps/bot/scripts/audio/lshuffle.lua
Normal file
@@ -0,0 +1,19 @@
|
||||
math.randomseed(tonumber(ARGV[1]))
|
||||
local function shuffle(t)
|
||||
for i = #t, 1, -1 do
|
||||
local rand = math.random(i)
|
||||
t[i], t[rand] = t[rand], t[i]
|
||||
end
|
||||
return t
|
||||
end
|
||||
|
||||
local KEY = KEYS[1]
|
||||
local list = redis.call('lrange', KEY, 0, -1)
|
||||
|
||||
if #list > 0 then
|
||||
shuffle(list)
|
||||
redis.call('del', KEY)
|
||||
redis.call('lpush', KEY, unpack(list))
|
||||
end
|
||||
|
||||
return 'OK'
|
||||
12
apps/bot/scripts/audio/rpopset.lua
Normal file
12
apps/bot/scripts/audio/rpopset.lua
Normal file
@@ -0,0 +1,12 @@
|
||||
local SOURCE = KEYS[1]
|
||||
local DESTINATION = KEYS[2]
|
||||
|
||||
local value = redis.call('rpop', SOURCE)
|
||||
|
||||
if value then
|
||||
redis.call('set', DESTINATION, value)
|
||||
return value
|
||||
end
|
||||
|
||||
redis.call('del', DESTINATION)
|
||||
return nil
|
||||
37
apps/bot/src/commands/gifs/amongus.ts
Normal file
37
apps/bot/src/commands/gifs/amongus.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import { Command } from '@sapphire/framework';
|
||||
import { env } from '../../env';
|
||||
|
||||
@ApplyOptions<Command.Options>({
|
||||
name: 'amongus',
|
||||
description: 'Replies with a random Among Us gif!',
|
||||
preconditions: ['isCommandDisabled']
|
||||
})
|
||||
export class AmongUsCommand extends Command {
|
||||
public override registerApplicationCommands(registry: Command.Registry) {
|
||||
registry.registerChatInputCommand(builder =>
|
||||
builder.setName(this.name).setDescription(this.description)
|
||||
);
|
||||
}
|
||||
|
||||
public override async chatInputRun(
|
||||
interaction: Command.ChatInputCommandInteraction
|
||||
) {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://tenor.googleapis.com/v2/search?key=${env.TENOR_API}&q=amongus&limit=1&random=true`
|
||||
);
|
||||
const json = await response.json();
|
||||
if (!json.results)
|
||||
return await interaction.reply({
|
||||
content: 'Something went wrong! Please try again later.'
|
||||
});
|
||||
|
||||
return await interaction.reply({ content: json.results[0].url });
|
||||
} catch (e) {
|
||||
return await interaction.reply({
|
||||
content: 'Something went wrong! Please try again later.'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
37
apps/bot/src/commands/gifs/anime.ts
Normal file
37
apps/bot/src/commands/gifs/anime.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import { Command } from '@sapphire/framework';
|
||||
import { env } from '../../env';
|
||||
|
||||
@ApplyOptions<Command.Options>({
|
||||
name: 'anime',
|
||||
description: 'Replies with a random anime gif!',
|
||||
preconditions: ['isCommandDisabled']
|
||||
})
|
||||
export class AnimeCommand extends Command {
|
||||
public override registerApplicationCommands(registry: Command.Registry) {
|
||||
registry.registerChatInputCommand(builder =>
|
||||
builder.setName(this.name).setDescription(this.description)
|
||||
);
|
||||
}
|
||||
|
||||
public override async chatInputRun(
|
||||
interaction: Command.ChatInputCommandInteraction
|
||||
) {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://tenor.googleapis.com/v2/search?key=${env.TENOR_API}&q=anime&limit=1&random=true`
|
||||
);
|
||||
const json = await response.json();
|
||||
if (!json.results)
|
||||
return await interaction.reply({
|
||||
content: 'Something went wrong! Please try again later.'
|
||||
});
|
||||
|
||||
return await interaction.reply({ content: json.results[0].url });
|
||||
} catch (e) {
|
||||
return await interaction.reply({
|
||||
content: 'Something went wrong! Please try again later.'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
37
apps/bot/src/commands/gifs/baka.ts
Normal file
37
apps/bot/src/commands/gifs/baka.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import { Command } from '@sapphire/framework';
|
||||
import { env } from '../../env';
|
||||
|
||||
@ApplyOptions<Command.Options>({
|
||||
name: 'baka',
|
||||
description: 'Replies with a random baka gif!',
|
||||
preconditions: ['isCommandDisabled']
|
||||
})
|
||||
export class BakaCommand extends Command {
|
||||
public override registerApplicationCommands(registry: Command.Registry) {
|
||||
registry.registerChatInputCommand(builder =>
|
||||
builder.setName(this.name).setDescription(this.description)
|
||||
);
|
||||
}
|
||||
|
||||
public override async chatInputRun(
|
||||
interaction: Command.ChatInputCommandInteraction
|
||||
) {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://tenor.googleapis.com/v2/search?key=${env.TENOR_API}&q=baka&limit=1&random=true`
|
||||
);
|
||||
const json = await response.json();
|
||||
if (!json.results)
|
||||
return await interaction.reply({
|
||||
content: 'Something went wrong! Please try again later.'
|
||||
});
|
||||
|
||||
return await interaction.reply({ content: json.results[0].url });
|
||||
} catch (e) {
|
||||
return await interaction.reply({
|
||||
content: 'Something went wrong! Please try again later.'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
37
apps/bot/src/commands/gifs/cat.ts
Normal file
37
apps/bot/src/commands/gifs/cat.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import { Command } from '@sapphire/framework';
|
||||
import { env } from '../../env';
|
||||
|
||||
@ApplyOptions<Command.Options>({
|
||||
name: 'cat',
|
||||
description: 'Replies with a random cat gif!',
|
||||
preconditions: ['isCommandDisabled']
|
||||
})
|
||||
export class CatCommand extends Command {
|
||||
public override registerApplicationCommands(registry: Command.Registry) {
|
||||
registry.registerChatInputCommand(builder =>
|
||||
builder.setName(this.name).setDescription(this.description)
|
||||
);
|
||||
}
|
||||
|
||||
public override async chatInputRun(
|
||||
interaction: Command.ChatInputCommandInteraction
|
||||
) {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://tenor.googleapis.com/v2/search?key=${env.TENOR_API}&q=cat&limit=1&random=true`
|
||||
);
|
||||
const json = await response.json();
|
||||
if (!json.results)
|
||||
return await interaction.reply({
|
||||
content: 'Something went wrong! Please try again later.'
|
||||
});
|
||||
|
||||
return await interaction.reply({ content: json.results[0].url });
|
||||
} catch (e) {
|
||||
return await interaction.reply({
|
||||
content: 'Something went wrong! Please try again later.'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
37
apps/bot/src/commands/gifs/doggo.ts
Normal file
37
apps/bot/src/commands/gifs/doggo.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import { Command } from '@sapphire/framework';
|
||||
import { env } from '../../env';
|
||||
|
||||
@ApplyOptions<Command.Options>({
|
||||
name: 'doggo',
|
||||
description: 'Replies with a random doggo gif!',
|
||||
preconditions: ['isCommandDisabled']
|
||||
})
|
||||
export class DoggoCommand extends Command {
|
||||
public override registerApplicationCommands(registry: Command.Registry) {
|
||||
registry.registerChatInputCommand(builder =>
|
||||
builder.setName(this.name).setDescription(this.description)
|
||||
);
|
||||
}
|
||||
|
||||
public override async chatInputRun(
|
||||
interaction: Command.ChatInputCommandInteraction
|
||||
) {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://tenor.googleapis.com/v2/search?key=${env.TENOR_API}&q=doggo&limit=1&random=true`
|
||||
);
|
||||
const json = await response.json();
|
||||
if (!json.results)
|
||||
return await interaction.reply({
|
||||
content: 'Something went wrong! Please try again later.'
|
||||
});
|
||||
|
||||
return await interaction.reply({ content: json.results[0].url });
|
||||
} catch (e) {
|
||||
return await interaction.reply({
|
||||
content: 'Something went wrong! Please try again later.'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
37
apps/bot/src/commands/gifs/gif.ts
Normal file
37
apps/bot/src/commands/gifs/gif.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import { Command } from '@sapphire/framework';
|
||||
import { env } from '../../env';
|
||||
|
||||
@ApplyOptions<Command.Options>({
|
||||
name: 'gif',
|
||||
description: 'Replies with a random gif gif!',
|
||||
preconditions: ['isCommandDisabled']
|
||||
})
|
||||
export class GifCommand extends Command {
|
||||
public override registerApplicationCommands(registry: Command.Registry) {
|
||||
registry.registerChatInputCommand(builder =>
|
||||
builder.setName(this.name).setDescription(this.description)
|
||||
);
|
||||
}
|
||||
|
||||
public override async chatInputRun(
|
||||
interaction: Command.ChatInputCommandInteraction
|
||||
) {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://tenor.googleapis.com/v2/search?key=${env.TENOR_API}&q=gif&limit=1&random=true`
|
||||
);
|
||||
const json = await response.json();
|
||||
if (!json.results)
|
||||
return await interaction.reply({
|
||||
content: 'Something went wrong! Please try again later.'
|
||||
});
|
||||
|
||||
return await interaction.reply({ content: json.results[0].url });
|
||||
} catch (e) {
|
||||
return await interaction.reply({
|
||||
content: 'Something went wrong! Please try again later.'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
37
apps/bot/src/commands/gifs/gintama.ts
Normal file
37
apps/bot/src/commands/gifs/gintama.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import { Command } from '@sapphire/framework';
|
||||
import { env } from '../../env';
|
||||
|
||||
@ApplyOptions<Command.Options>({
|
||||
name: 'gintama',
|
||||
description: 'Replies with a random gintama gif!',
|
||||
preconditions: ['isCommandDisabled']
|
||||
})
|
||||
export class GintamaCommand extends Command {
|
||||
public override registerApplicationCommands(registry: Command.Registry) {
|
||||
registry.registerChatInputCommand(builder =>
|
||||
builder.setName(this.name).setDescription(this.description)
|
||||
);
|
||||
}
|
||||
|
||||
public override async chatInputRun(
|
||||
interaction: Command.ChatInputCommandInteraction
|
||||
) {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://tenor.googleapis.com/v2/search?key=${env.TENOR_API}&q=gintama&limit=1&random=true`
|
||||
);
|
||||
const json = await response.json();
|
||||
if (!json.results)
|
||||
return await interaction.reply({
|
||||
content: 'Something went wrong! Please try again later.'
|
||||
});
|
||||
|
||||
return await interaction.reply({ content: json.results[0].url });
|
||||
} catch (e) {
|
||||
return await interaction.reply({
|
||||
content: 'Something went wrong! Please try again later.'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
37
apps/bot/src/commands/gifs/hug.ts
Normal file
37
apps/bot/src/commands/gifs/hug.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import { Command } from '@sapphire/framework';
|
||||
import { env } from '../../env';
|
||||
|
||||
@ApplyOptions<Command.Options>({
|
||||
name: 'hug',
|
||||
description: 'Replies with a random hug gif!',
|
||||
preconditions: ['isCommandDisabled']
|
||||
})
|
||||
export class HugCommand extends Command {
|
||||
public override registerApplicationCommands(registry: Command.Registry) {
|
||||
registry.registerChatInputCommand(builder =>
|
||||
builder.setName(this.name).setDescription(this.description)
|
||||
);
|
||||
}
|
||||
|
||||
public override async chatInputRun(
|
||||
interaction: Command.ChatInputCommandInteraction
|
||||
) {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://tenor.googleapis.com/v2/search?key=${env.TENOR_API}&q=hug&limit=1&random=true`
|
||||
);
|
||||
const json = await response.json();
|
||||
if (!json.results)
|
||||
return await interaction.reply({
|
||||
content: 'Something went wrong! Please try again later.'
|
||||
});
|
||||
|
||||
return await interaction.reply({ content: json.results[0].url });
|
||||
} catch (e) {
|
||||
return await interaction.reply({
|
||||
content: 'Something went wrong! Please try again later.'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
37
apps/bot/src/commands/gifs/jojo.ts
Normal file
37
apps/bot/src/commands/gifs/jojo.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import { Command } from '@sapphire/framework';
|
||||
import { env } from '../../env';
|
||||
|
||||
@ApplyOptions<Command.Options>({
|
||||
name: 'jojo',
|
||||
description: 'Replies with a random jojo gif!',
|
||||
preconditions: ['isCommandDisabled']
|
||||
})
|
||||
export class JojoCommand extends Command {
|
||||
public override registerApplicationCommands(registry: Command.Registry) {
|
||||
registry.registerChatInputCommand(builder =>
|
||||
builder.setName(this.name).setDescription(this.description)
|
||||
);
|
||||
}
|
||||
|
||||
public override async chatInputRun(
|
||||
interaction: Command.ChatInputCommandInteraction
|
||||
) {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://tenor.googleapis.com/v2/search?key=${env.TENOR_API}&q=jojo&limit=1&random=true`
|
||||
);
|
||||
const json = await response.json();
|
||||
if (!json.results)
|
||||
return await interaction.reply({
|
||||
content: 'Something went wrong! Please try again later.'
|
||||
});
|
||||
|
||||
return await interaction.reply({ content: json.results[0].url });
|
||||
} catch (e) {
|
||||
return await interaction.reply({
|
||||
content: 'Something went wrong! Please try again later.'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
37
apps/bot/src/commands/gifs/slap.ts
Normal file
37
apps/bot/src/commands/gifs/slap.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import { Command } from '@sapphire/framework';
|
||||
import { env } from '../../env';
|
||||
|
||||
@ApplyOptions<Command.Options>({
|
||||
name: 'slap',
|
||||
description: 'Replies with a random slap gif!',
|
||||
preconditions: ['isCommandDisabled']
|
||||
})
|
||||
export class SlapCommand extends Command {
|
||||
public override registerApplicationCommands(registry: Command.Registry) {
|
||||
registry.registerChatInputCommand(builder =>
|
||||
builder.setName(this.name).setDescription(this.description)
|
||||
);
|
||||
}
|
||||
|
||||
public override async chatInputRun(
|
||||
interaction: Command.ChatInputCommandInteraction
|
||||
) {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://tenor.googleapis.com/v2/search?key=${env.TENOR_API}&q=slap&limit=1&random=true`
|
||||
);
|
||||
const json = await response.json();
|
||||
if (!json.results)
|
||||
return await interaction.reply({
|
||||
content: 'Something went wrong! Please try again later.'
|
||||
});
|
||||
|
||||
return await interaction.reply({ content: json.results[0].url });
|
||||
} catch (e) {
|
||||
return await interaction.reply({
|
||||
content: 'Something went wrong! Please try again later.'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
37
apps/bot/src/commands/gifs/waifu.ts
Normal file
37
apps/bot/src/commands/gifs/waifu.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import { Command } from '@sapphire/framework';
|
||||
import { env } from '../../env';
|
||||
|
||||
@ApplyOptions<Command.Options>({
|
||||
name: 'waifu',
|
||||
description: 'Replies with a random waifu gif!',
|
||||
preconditions: ['isCommandDisabled']
|
||||
})
|
||||
export class WaifuCommand extends Command {
|
||||
public override registerApplicationCommands(registry: Command.Registry) {
|
||||
registry.registerChatInputCommand(builder =>
|
||||
builder.setName(this.name).setDescription(this.description)
|
||||
);
|
||||
}
|
||||
|
||||
public override async chatInputRun(
|
||||
interaction: Command.ChatInputCommandInteraction
|
||||
) {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://tenor.googleapis.com/v2/search?key=${env.TENOR_API}&q=waifu&limit=1&random=true`
|
||||
);
|
||||
const json = await response.json();
|
||||
if (!json.results)
|
||||
return await interaction.reply({
|
||||
content: 'Something went wrong! Please try again later.'
|
||||
});
|
||||
|
||||
return await interaction.reply({ content: json.results[0].url });
|
||||
} catch (e) {
|
||||
return await interaction.reply({
|
||||
content: 'Something went wrong! Please try again later.'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
52
apps/bot/src/commands/music/bassboost.ts
Normal file
52
apps/bot/src/commands/music/bassboost.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import { Command, CommandOptions } from '@sapphire/framework';
|
||||
import { container } from '@sapphire/framework';
|
||||
import type { Node, Player } from 'lavaclient';
|
||||
|
||||
@ApplyOptions<CommandOptions>({
|
||||
name: 'bassboost',
|
||||
description: 'Boost the bass of the playing track',
|
||||
preconditions: [
|
||||
'GuildOnly',
|
||||
'isCommandDisabled',
|
||||
'inVoiceChannel',
|
||||
'playerIsPlaying',
|
||||
'inPlayerVoiceChannel'
|
||||
]
|
||||
})
|
||||
export class BassboostCommand extends Command {
|
||||
public override registerApplicationCommands(
|
||||
registry: Command.Registry
|
||||
): void {
|
||||
registry.registerChatInputCommand({
|
||||
name: this.name,
|
||||
description: this.description
|
||||
});
|
||||
}
|
||||
public override async chatInputRun(
|
||||
interaction: Command.ChatInputCommandInteraction
|
||||
) {
|
||||
const { client } = container;
|
||||
|
||||
const player = client.music.players.get(
|
||||
interaction.guild!.id
|
||||
) as Player<Node>;
|
||||
|
||||
player.filters.equalizer = (player.bassboost = !player.bassboost)
|
||||
? [
|
||||
{ band: 0, gain: 0.55 },
|
||||
{ band: 1, gain: 0.45 },
|
||||
{ band: 2, gain: 0.4 },
|
||||
{ band: 3, gain: 0.3 },
|
||||
{ band: 4, gain: 0.15 },
|
||||
{ band: 5, gain: 0 },
|
||||
{ band: 6, gain: 0 }
|
||||
]
|
||||
: undefined;
|
||||
|
||||
await player.setFilters();
|
||||
return await interaction.reply(
|
||||
`Bassboost ${player.bassboost ? 'enabled' : 'disabled'}`
|
||||
);
|
||||
}
|
||||
}
|
||||
63
apps/bot/src/commands/music/create-playlist.ts
Normal file
63
apps/bot/src/commands/music/create-playlist.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import { Command, CommandOptions } from '@sapphire/framework';
|
||||
import { trpcNode } from '../../trpc';
|
||||
|
||||
@ApplyOptions<CommandOptions>({
|
||||
name: 'create-playlist',
|
||||
description: 'Create a custom playlist that you can play anytime',
|
||||
preconditions: [
|
||||
'GuildOnly',
|
||||
'isCommandDisabled',
|
||||
'userInDB',
|
||||
'playlistNotDuplicate'
|
||||
]
|
||||
})
|
||||
export class CreatePlaylistCommand extends Command {
|
||||
public override registerApplicationCommands(
|
||||
registry: Command.Registry
|
||||
): void {
|
||||
registry.registerChatInputCommand(builder =>
|
||||
builder
|
||||
.setName(this.name)
|
||||
.setDescription(this.description)
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('playlist-name')
|
||||
.setDescription(
|
||||
'What is the name of the playlist you want to create?'
|
||||
)
|
||||
.setRequired(true)
|
||||
)
|
||||
);
|
||||
}
|
||||
public override async chatInputRun(
|
||||
interaction: Command.ChatInputCommandInteraction
|
||||
) {
|
||||
const playlistName = interaction.options.getString('playlist-name', true);
|
||||
|
||||
const interactionMember = interaction.member?.user;
|
||||
|
||||
if (!interactionMember) {
|
||||
return await interaction.reply({
|
||||
content: ':x: Something went wrong! Please try again later'
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const playlist = await trpcNode.playlist.create.mutate({
|
||||
name: playlistName,
|
||||
userId: interactionMember.id
|
||||
});
|
||||
|
||||
if (!playlist) throw new Error();
|
||||
} catch (error) {
|
||||
await interaction.reply({
|
||||
content: `:x: You already have a playlist named **${playlistName}**`
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await interaction.reply(`Created a playlist named **${playlistName}**`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
65
apps/bot/src/commands/music/delete-playlist.ts
Normal file
65
apps/bot/src/commands/music/delete-playlist.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import { Command, CommandOptions } from '@sapphire/framework';
|
||||
import { trpcNode } from '../../trpc';
|
||||
import Logger from '../../lib/logger';
|
||||
|
||||
@ApplyOptions<CommandOptions>({
|
||||
name: 'delete-playlist',
|
||||
description: 'Delete a playlist from your saved playlists',
|
||||
preconditions: [
|
||||
'GuildOnly',
|
||||
'isCommandDisabled',
|
||||
'userInDB',
|
||||
'playlistExists'
|
||||
]
|
||||
})
|
||||
export class DeletePlaylistCommand extends Command {
|
||||
public override registerApplicationCommands(
|
||||
registry: Command.Registry
|
||||
): void {
|
||||
registry.registerChatInputCommand(builder =>
|
||||
builder
|
||||
.setName(this.name)
|
||||
.setDescription(this.description)
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('playlist-name')
|
||||
.setDescription(
|
||||
'What is the name of the playlist you want to delete?'
|
||||
)
|
||||
.setRequired(true)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public override async chatInputRun(
|
||||
interaction: Command.ChatInputCommandInteraction
|
||||
) {
|
||||
const playlistName = interaction.options.getString('playlist-name', true);
|
||||
|
||||
const interactionMember = interaction.member?.user;
|
||||
|
||||
if (!interactionMember) {
|
||||
return await interaction.reply(
|
||||
':x: Something went wrong! Please try again later'
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const playlist = await trpcNode.playlist.delete.mutate({
|
||||
name: playlistName,
|
||||
userId: interactionMember.id
|
||||
});
|
||||
|
||||
if (!playlist) throw new Error();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
Logger.error(error);
|
||||
return await interaction.reply(
|
||||
':x: Something went wrong! Please try again later'
|
||||
);
|
||||
}
|
||||
|
||||
return await interaction.reply(`:wastebasket: Deleted **${playlistName}**`);
|
||||
}
|
||||
}
|
||||
78
apps/bot/src/commands/music/display-playlist.ts
Normal file
78
apps/bot/src/commands/music/display-playlist.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import { Command, CommandOptions } from '@sapphire/framework';
|
||||
import { EmbedBuilder } from 'discord.js';
|
||||
import { PaginatedFieldMessageEmbed } from '@sapphire/discord.js-utilities';
|
||||
import { trpcNode } from '../../trpc';
|
||||
|
||||
@ApplyOptions<CommandOptions>({
|
||||
name: 'display-playlist',
|
||||
description: 'Display a saved playlist',
|
||||
preconditions: [
|
||||
'GuildOnly',
|
||||
'isCommandDisabled',
|
||||
'userInDB',
|
||||
'playlistExists'
|
||||
]
|
||||
})
|
||||
export class DisplayPlaylistCommand extends Command {
|
||||
public override registerApplicationCommands(
|
||||
registry: Command.Registry
|
||||
): void {
|
||||
registry.registerChatInputCommand(builder =>
|
||||
builder
|
||||
.setName(this.name)
|
||||
.setDescription(this.description)
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('playlist-name')
|
||||
.setDescription(
|
||||
'What is the name of the playlist you want to display?'
|
||||
)
|
||||
.setRequired(true)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public override async chatInputRun(
|
||||
interaction: Command.ChatInputCommandInteraction
|
||||
) {
|
||||
const playlistName = interaction.options.getString('playlist-name', true);
|
||||
|
||||
const interactionMember = interaction.member?.user;
|
||||
|
||||
if (!interactionMember) {
|
||||
return await interaction.reply({
|
||||
content: ':x: Something went wrong! Please try again later'
|
||||
});
|
||||
}
|
||||
|
||||
const playlistQuery = await trpcNode.playlist.getPlaylist.query({
|
||||
name: playlistName,
|
||||
userId: interactionMember.id
|
||||
});
|
||||
|
||||
const { playlist } = playlistQuery;
|
||||
|
||||
if (!playlist) {
|
||||
return await interaction.reply(
|
||||
':x: Something went wrong! Please try again soon'
|
||||
);
|
||||
}
|
||||
|
||||
const baseEmbed = new EmbedBuilder().setColor('Purple').setAuthor({
|
||||
name: interactionMember.username,
|
||||
iconURL: interactionMember.avatar || undefined
|
||||
});
|
||||
|
||||
new PaginatedFieldMessageEmbed()
|
||||
.setTitleField(`${playlistName} - Songs`)
|
||||
.setTemplate(baseEmbed)
|
||||
.setItems(playlist.songs)
|
||||
.formatItems((item: any) => `[${item.title}](${item.uri})`)
|
||||
.setItemsPerPage(5)
|
||||
.make()
|
||||
.run(interaction);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
50
apps/bot/src/commands/music/karaoke.ts
Normal file
50
apps/bot/src/commands/music/karaoke.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import { Command, CommandOptions } from '@sapphire/framework';
|
||||
import { container } from '@sapphire/framework';
|
||||
import type { Node, Player } from 'lavaclient';
|
||||
|
||||
@ApplyOptions<CommandOptions>({
|
||||
name: 'karaoke',
|
||||
description: 'Turn the playing track to karaoke',
|
||||
preconditions: [
|
||||
'GuildOnly',
|
||||
'isCommandDisabled',
|
||||
'inVoiceChannel',
|
||||
'playerIsPlaying',
|
||||
'inPlayerVoiceChannel'
|
||||
]
|
||||
})
|
||||
export class KaraokeCommand extends Command {
|
||||
public override registerApplicationCommands(
|
||||
registry: Command.Registry
|
||||
): void {
|
||||
registry.registerChatInputCommand({
|
||||
name: this.name,
|
||||
description: this.description
|
||||
});
|
||||
}
|
||||
|
||||
public override async chatInputRun(
|
||||
interaction: Command.ChatInputCommandInteraction
|
||||
) {
|
||||
const { client } = container;
|
||||
|
||||
const player = client.music.players.get(
|
||||
interaction.guild!.id
|
||||
) as Player<Node>;
|
||||
|
||||
player.filters.karaoke = (player.karaoke = !player.karaoke)
|
||||
? {
|
||||
level: 1,
|
||||
monoLevel: 1,
|
||||
filterBand: 220,
|
||||
filterWidth: 100
|
||||
}
|
||||
: undefined;
|
||||
|
||||
await player.setFilters();
|
||||
return await interaction.reply(
|
||||
`Karaoke ${player.karaoke ? 'enabled' : 'disabled'}`
|
||||
);
|
||||
}
|
||||
}
|
||||
37
apps/bot/src/commands/music/leave.ts
Normal file
37
apps/bot/src/commands/music/leave.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import { Command, CommandOptions } from '@sapphire/framework';
|
||||
import { container } from '@sapphire/framework';
|
||||
|
||||
@ApplyOptions<CommandOptions>({
|
||||
name: 'leave',
|
||||
description: 'Make the bot leave its voice channel and stop playing music',
|
||||
preconditions: [
|
||||
'GuildOnly',
|
||||
'isCommandDisabled',
|
||||
'inVoiceChannel',
|
||||
'playerIsPlaying',
|
||||
'inPlayerVoiceChannel'
|
||||
]
|
||||
})
|
||||
export class LeaveCommand extends Command {
|
||||
public override registerApplicationCommands(
|
||||
registry: Command.Registry
|
||||
): void {
|
||||
registry.registerChatInputCommand({
|
||||
name: this.name,
|
||||
description: this.description
|
||||
});
|
||||
}
|
||||
|
||||
public override async chatInputRun(
|
||||
interaction: Command.ChatInputCommandInteraction
|
||||
) {
|
||||
const { client } = container;
|
||||
|
||||
const queue = client.music.queues.get(interaction.guildId!);
|
||||
|
||||
await queue.leave();
|
||||
|
||||
await interaction.reply({ content: 'Left the voice channel.' });
|
||||
}
|
||||
}
|
||||
83
apps/bot/src/commands/music/lyrics.ts
Normal file
83
apps/bot/src/commands/music/lyrics.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import { Command, CommandOptions } from '@sapphire/framework';
|
||||
import { EmbedBuilder } from 'discord.js';
|
||||
import { container } from '@sapphire/framework';
|
||||
import { GeniusLyrics } from 'genius-discord-lyrics';
|
||||
import { PaginatedMessage } from '@sapphire/discord.js-utilities';
|
||||
import Logger from '../../lib/logger';
|
||||
|
||||
const genius = new GeniusLyrics(process.env.GENIUS_API || '');
|
||||
|
||||
@ApplyOptions<CommandOptions>({
|
||||
name: 'lyrics',
|
||||
description:
|
||||
'Get the lyrics of any song or the lyrics of the currently playing song!',
|
||||
preconditions: ['GuildOnly', 'isCommandDisabled']
|
||||
})
|
||||
export class LyricsCommand extends Command {
|
||||
public override registerApplicationCommands(
|
||||
registry: Command.Registry
|
||||
): void {
|
||||
registry.registerChatInputCommand(builder =>
|
||||
builder
|
||||
.setName(this.name)
|
||||
.setDescription(this.description)
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('title')
|
||||
.setDescription(':mag: What song lyrics would you like to get?')
|
||||
.setRequired(true)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public override async chatInputRun(
|
||||
interaction: Command.ChatInputCommandInteraction
|
||||
) {
|
||||
const { client } = container;
|
||||
let title = interaction.options.getString('title');
|
||||
|
||||
const player = client.music.players.get(interaction.guild!.id);
|
||||
|
||||
await interaction.deferReply();
|
||||
|
||||
if (!title) {
|
||||
if (!player) {
|
||||
return await interaction.followUp(
|
||||
'Please provide a valid song name or start playing one and try again!'
|
||||
);
|
||||
}
|
||||
//title = player.queue.current?.title as string;
|
||||
title = 'hi';
|
||||
}
|
||||
|
||||
try {
|
||||
const lyrics = (await genius.fetchLyrics(title)) as string;
|
||||
const lyricsIndex = Math.round(lyrics.length / 4096) + 1;
|
||||
const paginatedLyrics = new PaginatedMessage({
|
||||
template: new EmbedBuilder().setColor('Red').setTitle(title).setFooter({
|
||||
text: 'Provided by genius.com',
|
||||
iconURL:
|
||||
'https://assets.genius.com/images/apple-touch-icon.png?1652977688' // Genius Lyrics Icon
|
||||
})
|
||||
});
|
||||
|
||||
for (let i = 1; i <= lyricsIndex; ++i) {
|
||||
let b = i - 1;
|
||||
if (lyrics.trim().slice(b * 4096, i * 4096).length !== 0) {
|
||||
paginatedLyrics.addPageEmbed(embed => {
|
||||
return embed.setDescription(lyrics.slice(b * 4096, i * 4096));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await interaction.followUp('Lyrics generated');
|
||||
return paginatedLyrics.run(interaction);
|
||||
} catch (e) {
|
||||
Logger.error(e);
|
||||
return interaction.followUp(
|
||||
'Something when wrong when trying to fetch lyrics :('
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
70
apps/bot/src/commands/music/move.ts
Normal file
70
apps/bot/src/commands/music/move.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import { Command, CommandOptions } from '@sapphire/framework';
|
||||
import { container } from '@sapphire/framework';
|
||||
|
||||
@ApplyOptions<CommandOptions>({
|
||||
name: 'move',
|
||||
description: 'Move a track to a different position in queue',
|
||||
preconditions: [
|
||||
'GuildOnly',
|
||||
'isCommandDisabled',
|
||||
'inVoiceChannel',
|
||||
'playerIsPlaying',
|
||||
'inPlayerVoiceChannel'
|
||||
]
|
||||
})
|
||||
export class MoveCommand extends Command {
|
||||
public override registerApplicationCommands(
|
||||
registry: Command.Registry
|
||||
): void {
|
||||
registry.registerChatInputCommand(builder =>
|
||||
builder
|
||||
.setName(this.name)
|
||||
.setDescription(this.description)
|
||||
.addIntegerOption(option =>
|
||||
option
|
||||
.setName('current-position')
|
||||
.setDescription(
|
||||
'What is the position of the song you want to move?'
|
||||
)
|
||||
.setRequired(true)
|
||||
)
|
||||
.addIntegerOption(option =>
|
||||
option
|
||||
.setName('new-position')
|
||||
.setDescription(
|
||||
'What is the position you want to move the song to?'
|
||||
)
|
||||
.setRequired(true)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public override async chatInputRun(
|
||||
interaction: Command.ChatInputCommandInteraction
|
||||
) {
|
||||
const { client } = container;
|
||||
const currentPosition = interaction.options.getInteger(
|
||||
'current-position',
|
||||
true
|
||||
);
|
||||
const newPosition = interaction.options.getInteger('new-position', true);
|
||||
|
||||
const queue = client.music.queues.get(interaction.guildId!);
|
||||
const length = await queue.count();
|
||||
if (
|
||||
currentPosition < 1 ||
|
||||
currentPosition > length ||
|
||||
newPosition < 1 ||
|
||||
newPosition > length ||
|
||||
currentPosition == newPosition
|
||||
) {
|
||||
return await interaction.reply(
|
||||
':x: Please enter valid position numbers!'
|
||||
);
|
||||
}
|
||||
|
||||
await queue.moveTracks(currentPosition - 1, newPosition - 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
62
apps/bot/src/commands/music/my-playlists.ts
Normal file
62
apps/bot/src/commands/music/my-playlists.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import { Command, CommandOptions } from '@sapphire/framework';
|
||||
import { PaginatedFieldMessageEmbed } from '@sapphire/discord.js-utilities';
|
||||
import { EmbedBuilder } from 'discord.js';
|
||||
import { trpcNode } from '../../trpc';
|
||||
|
||||
@ApplyOptions<CommandOptions>({
|
||||
name: 'my-playlists',
|
||||
description: "Display your custom playlists' names",
|
||||
preconditions: [
|
||||
'GuildOnly',
|
||||
'isCommandDisabled',
|
||||
'inVoiceChannel',
|
||||
'userInDB'
|
||||
]
|
||||
})
|
||||
export class MyPlaylistsCommand extends Command {
|
||||
public override registerApplicationCommands(
|
||||
registry: Command.Registry
|
||||
): void {
|
||||
registry.registerChatInputCommand({
|
||||
name: this.name,
|
||||
description: this.description
|
||||
});
|
||||
}
|
||||
|
||||
public override async chatInputRun(
|
||||
interaction: Command.ChatInputCommandInteraction
|
||||
) {
|
||||
const interactionMember = interaction.member?.user;
|
||||
|
||||
if (!interactionMember) {
|
||||
return await interaction.reply({
|
||||
content: ':x: Something went wrong! Please try again later'
|
||||
});
|
||||
}
|
||||
|
||||
const baseEmbed = new EmbedBuilder().setColor('Purple').setAuthor({
|
||||
name: `${interactionMember.username}`,
|
||||
iconURL: interactionMember.avatar || undefined
|
||||
});
|
||||
|
||||
const playlistsQuery = await trpcNode.playlist.getAll.query({
|
||||
userId: interactionMember.id
|
||||
});
|
||||
|
||||
if (!playlistsQuery || !playlistsQuery.playlists.length) {
|
||||
return await interaction.reply(':x: You have no custom playlists');
|
||||
}
|
||||
|
||||
new PaginatedFieldMessageEmbed()
|
||||
.setTitleField('Custom Playlists')
|
||||
.setTemplate(baseEmbed)
|
||||
.setItems(playlistsQuery.playlists)
|
||||
.formatItems((playlist: any) => playlist.name)
|
||||
.setItemsPerPage(5)
|
||||
.make()
|
||||
.run(interaction);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
45
apps/bot/src/commands/music/nightcore.ts
Normal file
45
apps/bot/src/commands/music/nightcore.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import { Command, CommandOptions } from '@sapphire/framework';
|
||||
import { container } from '@sapphire/framework';
|
||||
import type { Node, Player } from 'lavaclient';
|
||||
|
||||
@ApplyOptions<CommandOptions>({
|
||||
name: 'nightcore',
|
||||
description: 'Enable/Disable Nightcore filter',
|
||||
preconditions: [
|
||||
'GuildOnly',
|
||||
'isCommandDisabled',
|
||||
'inVoiceChannel',
|
||||
'playerIsPlaying',
|
||||
'inPlayerVoiceChannel'
|
||||
]
|
||||
})
|
||||
export class NightcoreCommand extends Command {
|
||||
public override registerApplicationCommands(
|
||||
registry: Command.Registry
|
||||
): void {
|
||||
registry.registerChatInputCommand({
|
||||
name: this.name,
|
||||
description: this.description
|
||||
});
|
||||
}
|
||||
|
||||
public override async chatInputRun(
|
||||
interaction: Command.ChatInputCommandInteraction
|
||||
) {
|
||||
const { client } = container;
|
||||
|
||||
const player = client.music.players.get(
|
||||
interaction.guild!.id
|
||||
) as Player<Node>;
|
||||
|
||||
player.filters.timescale = (player.nightcore = !player.nightcore)
|
||||
? { speed: 1.125, pitch: 1.125, rate: 1 }
|
||||
: undefined;
|
||||
|
||||
await player.setFilters();
|
||||
return await interaction.reply(
|
||||
`Nightcore ${player.nightcore ? 'enabled' : 'disabled'}`
|
||||
);
|
||||
}
|
||||
}
|
||||
35
apps/bot/src/commands/music/pause.ts
Normal file
35
apps/bot/src/commands/music/pause.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import { Command, CommandOptions } from '@sapphire/framework';
|
||||
import { container } from '@sapphire/framework';
|
||||
|
||||
@ApplyOptions<CommandOptions>({
|
||||
name: 'pause',
|
||||
description: 'Pause the music',
|
||||
preconditions: [
|
||||
'GuildOnly',
|
||||
'isCommandDisabled',
|
||||
'inVoiceChannel',
|
||||
'playerIsPlaying',
|
||||
'inPlayerVoiceChannel'
|
||||
]
|
||||
})
|
||||
export class PauseCommand extends Command {
|
||||
public override registerApplicationCommands(
|
||||
registry: Command.Registry
|
||||
): void {
|
||||
registry.registerChatInputCommand({
|
||||
name: this.name,
|
||||
description: this.description
|
||||
});
|
||||
}
|
||||
|
||||
public override async chatInputRun(
|
||||
interaction: Command.ChatInputCommandInteraction
|
||||
) {
|
||||
const { client } = container;
|
||||
|
||||
const queue = client.music.queues.get(interaction.guildId!);
|
||||
|
||||
await queue.pause(interaction);
|
||||
}
|
||||
}
|
||||
157
apps/bot/src/commands/music/play.ts
Normal file
157
apps/bot/src/commands/music/play.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import { Command, CommandOptions } from '@sapphire/framework';
|
||||
import { container } from '@sapphire/framework';
|
||||
import searchSong from '../../lib/music/searchSong';
|
||||
import type { Song } from '../../lib/music/classes/Song';
|
||||
import { trpcNode } from '../../trpc';
|
||||
import { GuildMember } from 'discord.js';
|
||||
|
||||
@ApplyOptions<CommandOptions>({
|
||||
name: 'play',
|
||||
description: 'Play any song or playlist from YouTube, Spotify and more!',
|
||||
preconditions: [
|
||||
'GuildOnly',
|
||||
'isCommandDisabled',
|
||||
'inVoiceChannel',
|
||||
'inPlayerVoiceChannel'
|
||||
]
|
||||
})
|
||||
export class PlayCommand extends Command {
|
||||
public override registerApplicationCommands(
|
||||
registry: Command.Registry
|
||||
): void {
|
||||
registry.registerChatInputCommand(builder =>
|
||||
builder
|
||||
.setName(this.name)
|
||||
.setDescription(this.description)
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('query')
|
||||
.setDescription(
|
||||
'What song or playlist would you like to listen to?'
|
||||
)
|
||||
.setRequired(true)
|
||||
)
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('is-custom-playlist')
|
||||
.setDescription('Is it a custom playlist?')
|
||||
.addChoices(
|
||||
{
|
||||
name: 'Yes',
|
||||
value: 'Yes'
|
||||
},
|
||||
{
|
||||
name: 'No',
|
||||
value: 'No'
|
||||
}
|
||||
)
|
||||
)
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('shuffle-playlist')
|
||||
.setDescription('Would you like to shuffle the playlist?')
|
||||
.addChoices(
|
||||
{
|
||||
name: 'Yes',
|
||||
value: 'Yes'
|
||||
},
|
||||
{
|
||||
name: 'No',
|
||||
value: 'No'
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public override async chatInputRun(
|
||||
interaction: Command.ChatInputCommandInteraction
|
||||
) {
|
||||
await interaction.deferReply();
|
||||
|
||||
const { client } = container;
|
||||
|
||||
const query = interaction.options.getString('query', true);
|
||||
const isCustomPlaylist =
|
||||
interaction.options.getString('is-custom-playlist');
|
||||
|
||||
const shufflePlaylist = interaction.options.getString('shuffle-playlist');
|
||||
|
||||
const interactionMember = interaction.member?.user;
|
||||
|
||||
if (!interactionMember) {
|
||||
return await interaction.followUp(
|
||||
':x: Something went wrong! Please try again later'
|
||||
);
|
||||
}
|
||||
|
||||
const { music } = client;
|
||||
|
||||
const voiceChannel = (interaction.member as GuildMember).voice.channel;
|
||||
|
||||
// edge case - someome initiated the command but left the voice channel
|
||||
if (!voiceChannel) {
|
||||
return interaction.followUp({
|
||||
content: ':x: You need to be in a voice channel to use this command!'
|
||||
});
|
||||
}
|
||||
|
||||
let queue = music.queues.get(interaction.guildId!);
|
||||
await queue.setTextChannelID(interaction.channel!.id);
|
||||
|
||||
if (!queue.player) {
|
||||
const player = queue.createPlayer();
|
||||
await player.connect(voiceChannel.id, { deafened: true });
|
||||
}
|
||||
|
||||
let tracks: Song[] = [];
|
||||
let message: string = '';
|
||||
|
||||
if (isCustomPlaylist == 'Yes') {
|
||||
const data = await trpcNode.playlist.getPlaylist.query({
|
||||
userId: interactionMember.id,
|
||||
name: query
|
||||
});
|
||||
|
||||
const { playlist } = data;
|
||||
|
||||
if (!playlist) {
|
||||
return await interaction.followUp(`:x: You have no such playlist!`);
|
||||
}
|
||||
if (!playlist.songs.length) {
|
||||
return await interaction.followUp(`:x: **${query}** is empty!`);
|
||||
}
|
||||
|
||||
const { songs } = playlist;
|
||||
tracks.push(...songs);
|
||||
message = `Added songs from **${playlist}** to the queue!`;
|
||||
} else {
|
||||
const trackTuple = await searchSong(query, interaction.user);
|
||||
if (!trackTuple[1].length) {
|
||||
return await interaction.followUp({ content: trackTuple[0] as string }); // error
|
||||
}
|
||||
message = trackTuple[0];
|
||||
tracks.push(...trackTuple[1]);
|
||||
}
|
||||
|
||||
await queue.add(tracks);
|
||||
if (shufflePlaylist == 'Yes') {
|
||||
await queue.shuffleTracks();
|
||||
}
|
||||
|
||||
const current = await queue.getCurrentTrack();
|
||||
if (current) {
|
||||
client.emit(
|
||||
'musicSongPlayMessage',
|
||||
interaction.channel,
|
||||
await queue.getCurrentTrack()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
queue.start();
|
||||
|
||||
return await interaction.followUp({ content: message });
|
||||
}
|
||||
}
|
||||
50
apps/bot/src/commands/music/queue.ts
Normal file
50
apps/bot/src/commands/music/queue.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import { Command, CommandOptions } from '@sapphire/framework';
|
||||
import { EmbedBuilder } from 'discord.js';
|
||||
import { container } from '@sapphire/framework';
|
||||
import { PaginatedFieldMessageEmbed } from '@sapphire/discord.js-utilities';
|
||||
|
||||
@ApplyOptions<CommandOptions>({
|
||||
name: 'queue',
|
||||
description: 'Get a List of the Music Queue',
|
||||
preconditions: [
|
||||
'GuildOnly',
|
||||
'isCommandDisabled',
|
||||
'inVoiceChannel',
|
||||
'playerIsPlaying',
|
||||
'inPlayerVoiceChannel'
|
||||
]
|
||||
})
|
||||
export class QueueCommand extends Command {
|
||||
public override registerApplicationCommands(
|
||||
registry: Command.Registry
|
||||
): void {
|
||||
registry.registerChatInputCommand({
|
||||
name: this.name,
|
||||
description: this.description
|
||||
});
|
||||
}
|
||||
|
||||
public override async chatInputRun(
|
||||
interaction: Command.ChatInputCommandInteraction
|
||||
) {
|
||||
const { client } = container;
|
||||
const queue = client.music.queues.get(interaction.guildId!);
|
||||
const baseEmbed = new EmbedBuilder().setColor('Red').setAuthor({
|
||||
name: `${interaction.user.username}`,
|
||||
iconURL: interaction.user.displayAvatarURL()
|
||||
});
|
||||
let index = 1;
|
||||
new PaginatedFieldMessageEmbed()
|
||||
.setTitleField('Queue')
|
||||
.setTemplate(baseEmbed)
|
||||
.setItems(await queue.tracks())
|
||||
.formatItems(
|
||||
(queueList: any) =>
|
||||
`${index++}) ***[${queueList.title}](${queueList.uri})***`
|
||||
)
|
||||
.setItemsPerPage(10)
|
||||
.make()
|
||||
.run(interaction);
|
||||
}
|
||||
}
|
||||
94
apps/bot/src/commands/music/remove-from-playlist.ts
Normal file
94
apps/bot/src/commands/music/remove-from-playlist.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import { Command, CommandOptions } from '@sapphire/framework';
|
||||
import { trpcNode } from '../../trpc';
|
||||
|
||||
@ApplyOptions<CommandOptions>({
|
||||
name: 'remove-from-playlist',
|
||||
description: 'Remove a song from a saved playlist',
|
||||
preconditions: [
|
||||
'GuildOnly',
|
||||
'isCommandDisabled',
|
||||
'userInDB',
|
||||
'playlistExists'
|
||||
]
|
||||
})
|
||||
export class RemoveFromPlaylistCommand extends Command {
|
||||
public override registerApplicationCommands(
|
||||
registry: Command.Registry
|
||||
): void {
|
||||
registry.registerChatInputCommand(builder =>
|
||||
builder
|
||||
.setName(this.name)
|
||||
.setDescription(this.description)
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('playlist-name')
|
||||
.setDescription(
|
||||
'What is the name of the playlist you want to remove from?'
|
||||
)
|
||||
.setRequired(true)
|
||||
)
|
||||
.addIntegerOption(option =>
|
||||
option
|
||||
.setName('location')
|
||||
.setDescription(
|
||||
'What is the index of the video you would like to delete from your saved playlist?'
|
||||
)
|
||||
.setRequired(true)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public override async chatInputRun(
|
||||
interaction: Command.ChatInputCommandInteraction
|
||||
) {
|
||||
await interaction.deferReply();
|
||||
const playlistName = interaction.options.getString('playlist-name', true);
|
||||
const location = interaction.options.getInteger('location', true);
|
||||
|
||||
const interactionMember = interaction.member?.user;
|
||||
|
||||
if (!interactionMember) {
|
||||
return await interaction.followUp(
|
||||
':x: Something went wrong! Please try again later'
|
||||
);
|
||||
}
|
||||
|
||||
let playlist;
|
||||
try {
|
||||
const playlistQuery = await trpcNode.playlist.getPlaylist.query({
|
||||
name: playlistName,
|
||||
userId: interactionMember.id
|
||||
});
|
||||
|
||||
playlist = playlistQuery.playlist;
|
||||
} catch (error) {
|
||||
return await interaction.followUp(':x: Something went wrong!');
|
||||
}
|
||||
|
||||
const songs = playlist?.songs;
|
||||
|
||||
if (!songs?.length) {
|
||||
return await interaction.followUp(`:x: **${playlistName}** is empty!`);
|
||||
}
|
||||
|
||||
if (location > songs.length || location < 0) {
|
||||
return await interaction.followUp(':x: Please enter a valid index!');
|
||||
}
|
||||
|
||||
const id = songs[location - 1].id;
|
||||
|
||||
const song = await trpcNode.song.delete.mutate({
|
||||
id
|
||||
});
|
||||
|
||||
if (!song) {
|
||||
return await interaction.followUp(':x: Something went wrong!');
|
||||
}
|
||||
|
||||
await interaction.followUp(
|
||||
`:wastebasket: Deleted **${song.song.title}** from **${playlistName}**`
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
52
apps/bot/src/commands/music/remove.ts
Normal file
52
apps/bot/src/commands/music/remove.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import { Command, CommandOptions } from '@sapphire/framework';
|
||||
import { container } from '@sapphire/framework';
|
||||
|
||||
@ApplyOptions<CommandOptions>({
|
||||
name: 'remove',
|
||||
description: 'Remove a track from the queue',
|
||||
preconditions: [
|
||||
'GuildOnly',
|
||||
'isCommandDisabled',
|
||||
'inVoiceChannel',
|
||||
'playerIsPlaying',
|
||||
'inPlayerVoiceChannel'
|
||||
]
|
||||
})
|
||||
export class RemoveCommand extends Command {
|
||||
public override registerApplicationCommands(
|
||||
registry: Command.Registry
|
||||
): void {
|
||||
registry.registerChatInputCommand(builder =>
|
||||
builder
|
||||
.setName(this.name)
|
||||
.setDescription(this.description)
|
||||
.addIntegerOption(option =>
|
||||
option
|
||||
.setName('position')
|
||||
.setDescription(
|
||||
'What is the position of the song you want to remove from the queue?'
|
||||
)
|
||||
.setRequired(true)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public override async chatInputRun(
|
||||
interaction: Command.ChatInputCommandInteraction
|
||||
) {
|
||||
const { client } = container;
|
||||
const position = interaction.options.getInteger('position', true);
|
||||
|
||||
const queue = client.music.queues.get(interaction.guildId!);
|
||||
const length = await queue.count();
|
||||
if (position < 1 || position > length) {
|
||||
return interaction.reply(':x: Please enter a valid position number!');
|
||||
}
|
||||
|
||||
await queue.removeAt(position - 1);
|
||||
return await interaction.reply({
|
||||
content: `Removed track at position ${position}`
|
||||
});
|
||||
}
|
||||
}
|
||||
35
apps/bot/src/commands/music/resume.ts
Normal file
35
apps/bot/src/commands/music/resume.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import { Command, CommandOptions } from '@sapphire/framework';
|
||||
import { container } from '@sapphire/framework';
|
||||
|
||||
@ApplyOptions<CommandOptions>({
|
||||
name: 'resume',
|
||||
description: 'Resume the music',
|
||||
preconditions: [
|
||||
'GuildOnly',
|
||||
'isCommandDisabled',
|
||||
'inVoiceChannel',
|
||||
'playerIsPlaying',
|
||||
'inPlayerVoiceChannel'
|
||||
]
|
||||
})
|
||||
export class ResumeCommand extends Command {
|
||||
public override registerApplicationCommands(
|
||||
registry: Command.Registry
|
||||
): void {
|
||||
registry.registerChatInputCommand({
|
||||
name: this.name,
|
||||
description: this.description
|
||||
});
|
||||
}
|
||||
|
||||
public override async chatInputRun(
|
||||
interaction: Command.ChatInputCommandInteraction
|
||||
) {
|
||||
const { client } = container;
|
||||
|
||||
const queue = client.music.queues.get(interaction.guildId!);
|
||||
|
||||
await queue.resume(interaction);
|
||||
}
|
||||
}
|
||||
96
apps/bot/src/commands/music/save-to-playlist.ts
Normal file
96
apps/bot/src/commands/music/save-to-playlist.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import { Command, CommandOptions } from '@sapphire/framework';
|
||||
import searchSong from '../../lib/music/searchSong';
|
||||
import { trpcNode } from '../../trpc';
|
||||
import Logger from '../../lib/logger';
|
||||
|
||||
@ApplyOptions<CommandOptions>({
|
||||
name: 'save-to-playlist',
|
||||
description: 'Save a song or a playlist to a custom playlist',
|
||||
preconditions: [
|
||||
'GuildOnly',
|
||||
'isCommandDisabled',
|
||||
'userInDB',
|
||||
'playlistExists'
|
||||
]
|
||||
})
|
||||
export class SaveToPlaylistCommand extends Command {
|
||||
public override registerApplicationCommands(
|
||||
registry: Command.Registry
|
||||
): void {
|
||||
registry.registerChatInputCommand(builder =>
|
||||
builder
|
||||
.setName(this.name)
|
||||
.setDescription(this.description)
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('playlist-name')
|
||||
.setDescription(
|
||||
'What is the name of the playlist you want to save to?'
|
||||
)
|
||||
.setRequired(true)
|
||||
)
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('url')
|
||||
.setDescription('What do you want to save to the custom playlist?')
|
||||
.setRequired(true)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public override async chatInputRun(
|
||||
interaction: Command.ChatInputCommandInteraction
|
||||
) {
|
||||
await interaction.deferReply();
|
||||
const playlistName = interaction.options.getString('playlist-name', true);
|
||||
const url = interaction.options.getString('url', true);
|
||||
|
||||
const interactionMember = interaction.member?.user;
|
||||
|
||||
if (!interactionMember) {
|
||||
return await interaction.followUp(
|
||||
':x: Something went wrong! Please try again later'
|
||||
);
|
||||
}
|
||||
|
||||
const playlistQuery = await trpcNode.playlist.getPlaylist.query({
|
||||
name: playlistName,
|
||||
userId: interactionMember.id
|
||||
});
|
||||
|
||||
if (!playlistQuery.playlist) {
|
||||
return await interaction.followUp('Playlist does not exist');
|
||||
}
|
||||
|
||||
const playlistId = playlistQuery.playlist.id;
|
||||
|
||||
const songTuple = await searchSong(url, interaction.user);
|
||||
if (!songTuple[1].length) {
|
||||
return await interaction.followUp(songTuple[0]);
|
||||
}
|
||||
|
||||
const songArray = songTuple[1];
|
||||
const songsToAdd: any[] = [];
|
||||
|
||||
for (let i = 0; i < songArray.length; i++) {
|
||||
const song = songArray[i];
|
||||
delete song['requester'];
|
||||
songsToAdd.push({
|
||||
...song,
|
||||
playlistId: +playlistId
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await trpcNode.song.createMany.mutate({
|
||||
songs: songsToAdd
|
||||
});
|
||||
|
||||
return await interaction.followUp(`Added tracks to **${playlistName}**`);
|
||||
} catch (error) {
|
||||
Logger.error(error);
|
||||
return await interaction.followUp(':x: Something went wrong!');
|
||||
}
|
||||
}
|
||||
}
|
||||
58
apps/bot/src/commands/music/seek.ts
Normal file
58
apps/bot/src/commands/music/seek.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import { Command, CommandOptions } from '@sapphire/framework';
|
||||
import { container } from '@sapphire/framework';
|
||||
|
||||
@ApplyOptions<CommandOptions>({
|
||||
name: 'seek',
|
||||
description: 'Seek to a desired point in a track',
|
||||
preconditions: [
|
||||
'GuildOnly',
|
||||
'isCommandDisabled',
|
||||
'inVoiceChannel',
|
||||
'playerIsPlaying',
|
||||
'inPlayerVoiceChannel'
|
||||
]
|
||||
})
|
||||
export class SeekCommand extends Command {
|
||||
public override registerApplicationCommands(
|
||||
registry: Command.Registry
|
||||
): void {
|
||||
registry.registerChatInputCommand(builder =>
|
||||
builder
|
||||
.setName(this.name)
|
||||
.setDescription(this.description)
|
||||
.addIntegerOption(option =>
|
||||
option
|
||||
.setName('seconds')
|
||||
.setDescription(
|
||||
'To what point in the track do you want to seek? (in seconds)'
|
||||
)
|
||||
.setRequired(true)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public override async chatInputRun(
|
||||
interaction: Command.ChatInputCommandInteraction
|
||||
) {
|
||||
const { client } = container;
|
||||
const seconds = interaction.options.getInteger('seconds', true);
|
||||
const milliseconds = seconds * 1000;
|
||||
|
||||
const queue = client.music.queues.get(interaction.guildId!);
|
||||
const track = await queue.getCurrentTrack();
|
||||
if (!track)
|
||||
return await interaction.reply(':x: There is no track playing!'); // should never happen
|
||||
if (!track.isSeekable)
|
||||
return await interaction.reply(':x: This track is not seekable!');
|
||||
|
||||
if (milliseconds > track.length || milliseconds < 0) {
|
||||
return await interaction.reply(':x: Please enter a valid number!');
|
||||
}
|
||||
|
||||
const player = queue.player;
|
||||
await player.seek(milliseconds);
|
||||
|
||||
return await interaction.reply(`Seeked to ${seconds} seconds`);
|
||||
}
|
||||
}
|
||||
41
apps/bot/src/commands/music/shuffle.ts
Normal file
41
apps/bot/src/commands/music/shuffle.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import { Command, CommandOptions } from '@sapphire/framework';
|
||||
import { container } from '@sapphire/framework';
|
||||
|
||||
@ApplyOptions<CommandOptions>({
|
||||
name: 'shuffle',
|
||||
description: 'Shuffle the music queue',
|
||||
preconditions: [
|
||||
'GuildOnly',
|
||||
'isCommandDisabled',
|
||||
'inVoiceChannel',
|
||||
'playerIsPlaying',
|
||||
'inPlayerVoiceChannel'
|
||||
]
|
||||
})
|
||||
export class LeaveCommand extends Command {
|
||||
public override registerApplicationCommands(
|
||||
registry: Command.Registry
|
||||
): void {
|
||||
registry.registerChatInputCommand({
|
||||
name: this.name,
|
||||
description: this.description
|
||||
});
|
||||
}
|
||||
|
||||
public override async chatInputRun(
|
||||
interaction: Command.ChatInputCommandInteraction
|
||||
) {
|
||||
const { client } = container;
|
||||
|
||||
const queue = client.music.queues.get(interaction.guildId!);
|
||||
|
||||
if (!(await queue.count())) {
|
||||
return await interaction.reply(':x: There are no songs in queue!');
|
||||
}
|
||||
|
||||
await queue.shuffleTracks();
|
||||
|
||||
return await interaction.reply(':white_check_mark: Shuffled queue!');
|
||||
}
|
||||
}
|
||||
40
apps/bot/src/commands/music/skip.ts
Normal file
40
apps/bot/src/commands/music/skip.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import { Command, CommandOptions } from '@sapphire/framework';
|
||||
import { container } from '@sapphire/framework';
|
||||
|
||||
@ApplyOptions<CommandOptions>({
|
||||
name: 'skip',
|
||||
description: 'Skip the current song playing',
|
||||
preconditions: [
|
||||
'GuildOnly',
|
||||
'isCommandDisabled',
|
||||
'inVoiceChannel',
|
||||
'playerIsPlaying',
|
||||
'inPlayerVoiceChannel'
|
||||
]
|
||||
})
|
||||
export class SkipCommand extends Command {
|
||||
public override registerApplicationCommands(
|
||||
registry: Command.Registry
|
||||
): void {
|
||||
registry.registerChatInputCommand({
|
||||
name: this.name,
|
||||
description: this.description
|
||||
});
|
||||
}
|
||||
|
||||
public override async chatInputRun(
|
||||
interaction: Command.ChatInputCommandInteraction
|
||||
) {
|
||||
const { client } = container;
|
||||
const { music } = client;
|
||||
const queue = music.queues.get(interaction.guildId!);
|
||||
|
||||
const track = await queue.getCurrentTrack();
|
||||
await queue.next({ skipped: true });
|
||||
|
||||
client.emit('musicSongSkipNotify', interaction, track);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
57
apps/bot/src/commands/music/skipto.ts
Normal file
57
apps/bot/src/commands/music/skipto.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import { Command, CommandOptions } from '@sapphire/framework';
|
||||
import { container } from '@sapphire/framework';
|
||||
|
||||
@ApplyOptions<CommandOptions>({
|
||||
name: 'skipto',
|
||||
description: 'Skip to a track in queue',
|
||||
preconditions: [
|
||||
'GuildOnly',
|
||||
'isCommandDisabled',
|
||||
'inVoiceChannel',
|
||||
'playerIsPlaying',
|
||||
'inPlayerVoiceChannel'
|
||||
]
|
||||
})
|
||||
export class SkipToCommand extends Command {
|
||||
public override registerApplicationCommands(
|
||||
registry: Command.Registry
|
||||
): void {
|
||||
registry.registerChatInputCommand(builder =>
|
||||
builder
|
||||
.setName(this.name)
|
||||
.setDescription(this.description)
|
||||
.addIntegerOption(option =>
|
||||
option
|
||||
.setName('position')
|
||||
.setDescription(
|
||||
'What is the position of the song you want to skip to in queue?'
|
||||
)
|
||||
.setRequired(true)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public override async chatInputRun(
|
||||
interaction: Command.ChatInputCommandInteraction
|
||||
) {
|
||||
const { client } = container;
|
||||
const position = interaction.options.getInteger('position', true);
|
||||
|
||||
const queue = client.music.queues.get(interaction.guildId!);
|
||||
const length = await queue.count();
|
||||
if (position > length || position < 1) {
|
||||
return await interaction.reply(
|
||||
':x: Please enter a valid track position.'
|
||||
);
|
||||
}
|
||||
|
||||
await queue.skipTo(position);
|
||||
|
||||
await interaction.reply(
|
||||
`:white_check_mark: Skipped to track number ${position}!`
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
58
apps/bot/src/commands/music/vaporwave.ts
Normal file
58
apps/bot/src/commands/music/vaporwave.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import { Command, CommandOptions } from '@sapphire/framework';
|
||||
import { container } from '@sapphire/framework';
|
||||
import type { Node, Player } from 'lavaclient';
|
||||
|
||||
@ApplyOptions<CommandOptions>({
|
||||
name: 'vaporwave',
|
||||
description: 'Apply vaporwave on the playing track!',
|
||||
preconditions: [
|
||||
'GuildOnly',
|
||||
'isCommandDisabled',
|
||||
'inVoiceChannel',
|
||||
'playerIsPlaying',
|
||||
'inPlayerVoiceChannel'
|
||||
]
|
||||
})
|
||||
export class VaporWaveCommand extends Command {
|
||||
public override registerApplicationCommands(
|
||||
registry: Command.Registry
|
||||
): void {
|
||||
registry.registerChatInputCommand({
|
||||
name: this.name,
|
||||
description: this.description
|
||||
});
|
||||
}
|
||||
|
||||
public override async chatInputRun(
|
||||
interaction: Command.ChatInputCommandInteraction
|
||||
) {
|
||||
const { client } = container;
|
||||
|
||||
const player = client.music.players.get(
|
||||
interaction.guild!.id
|
||||
) as Player<Node>;
|
||||
|
||||
player.filters = (player.vaporwave = !player.vaporwave)
|
||||
? {
|
||||
...player.filters,
|
||||
equalizer: [
|
||||
{ band: 1, gain: 0.7 },
|
||||
{ band: 0, gain: 0.6 }
|
||||
],
|
||||
timescale: { pitch: 0.7, speed: 1, rate: 1 },
|
||||
tremolo: { depth: 0.6, frequency: 14 }
|
||||
}
|
||||
: {
|
||||
...player.filters,
|
||||
equalizer: undefined,
|
||||
timescale: undefined,
|
||||
tremolo: undefined
|
||||
};
|
||||
|
||||
await player.setFilters();
|
||||
return await interaction.reply(
|
||||
`Vaporwave ${player.vaporwave ? 'enabled' : 'disabled'}`
|
||||
);
|
||||
}
|
||||
}
|
||||
51
apps/bot/src/commands/music/volume.ts
Normal file
51
apps/bot/src/commands/music/volume.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import { Command, CommandOptions } from '@sapphire/framework';
|
||||
import { container } from '@sapphire/framework';
|
||||
|
||||
@ApplyOptions<CommandOptions>({
|
||||
name: 'volume',
|
||||
description: 'Set the Volume',
|
||||
preconditions: [
|
||||
'GuildOnly',
|
||||
'isCommandDisabled',
|
||||
'inVoiceChannel',
|
||||
'playerIsPlaying',
|
||||
'inPlayerVoiceChannel'
|
||||
]
|
||||
})
|
||||
export class VolumeCommand extends Command {
|
||||
public override registerApplicationCommands(
|
||||
registry: Command.Registry
|
||||
): void {
|
||||
registry.registerChatInputCommand(builder =>
|
||||
builder
|
||||
.setName(this.name)
|
||||
.setDescription(this.description)
|
||||
.addNumberOption(option =>
|
||||
option
|
||||
.setName('setting')
|
||||
.setDescription('What Volume? (0 to 200)')
|
||||
.setRequired(true)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public override async chatInputRun(
|
||||
interaction: Command.ChatInputCommandInteraction
|
||||
) {
|
||||
const { client } = container;
|
||||
const query = interaction.options.getNumber('setting', true);
|
||||
|
||||
const queue = client.music.queues.get(interaction.guildId!);
|
||||
|
||||
if (query > 200 || query < 0) {
|
||||
return await interaction.reply(':x: Volume must be between 0 and 200!');
|
||||
}
|
||||
|
||||
await queue.setVolume(query);
|
||||
|
||||
return await interaction.reply(
|
||||
`:white_check_mark: Volume set to ${query}!`
|
||||
);
|
||||
}
|
||||
}
|
||||
72
apps/bot/src/commands/other/8ball.ts
Normal file
72
apps/bot/src/commands/other/8ball.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import { Command } from '@sapphire/framework';
|
||||
import { EmbedBuilder } from 'discord.js';
|
||||
|
||||
@ApplyOptions<Command.Options>({
|
||||
name: '8ball',
|
||||
description: 'Get the answer to anything!',
|
||||
preconditions: ['isCommandDisabled']
|
||||
})
|
||||
export class EightBallCommand extends Command {
|
||||
public override registerApplicationCommands(registry: Command.Registry) {
|
||||
registry.registerChatInputCommand(builder =>
|
||||
builder //
|
||||
.setName(this.name)
|
||||
.setDescription(this.description)
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('question')
|
||||
.setDescription('The question you want to ask the 8ball')
|
||||
.setRequired(true)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public override async chatInputRun(
|
||||
interaction: Command.ChatInputCommandInteraction
|
||||
) {
|
||||
const question = interaction.options.getString('question', true);
|
||||
if (question.length > 255) {
|
||||
return await interaction.reply({
|
||||
content:
|
||||
'Your question is too long! Please keep it under 255 characters.'
|
||||
});
|
||||
}
|
||||
|
||||
const randomAnswer = answers[Math.floor(Math.random() * answers.length)];
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle(question)
|
||||
.setAuthor({
|
||||
name: 'Magic 8ball',
|
||||
iconURL: 'https://i.imgur.com/HbwMhWM.png'
|
||||
})
|
||||
.setDescription(randomAnswer)
|
||||
.setColor('DarkButNotBlack')
|
||||
.setTimestamp();
|
||||
|
||||
return interaction.reply({ embeds: [embed] });
|
||||
}
|
||||
}
|
||||
|
||||
const answers = [
|
||||
'Yes.',
|
||||
'No.',
|
||||
'My sources say yes!',
|
||||
'Most likely.',
|
||||
"I don't know.",
|
||||
'Maybe, sometimes.',
|
||||
'Outlook is good.',
|
||||
'Signs point to yes.',
|
||||
'Definitely!',
|
||||
'Absolutely!',
|
||||
'Nope.',
|
||||
"No thanks, I won't be able to make it.",
|
||||
'No Way!',
|
||||
"It's certain.",
|
||||
"It's decidedly so.",
|
||||
'Without a doubt.',
|
||||
'Yes - definitely.',
|
||||
'You can rely on it.',
|
||||
'As I see it, yes.'
|
||||
];
|
||||
31
apps/bot/src/commands/other/about.ts
Normal file
31
apps/bot/src/commands/other/about.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import { Command } from '@sapphire/framework';
|
||||
import { EmbedBuilder } from 'discord.js';
|
||||
|
||||
@ApplyOptions<Command.Options>({
|
||||
name: 'about',
|
||||
description: 'Display info about the bot!',
|
||||
preconditions: ['isCommandDisabled']
|
||||
})
|
||||
export class AboutCommand extends Command {
|
||||
public override registerApplicationCommands(registry: Command.Registry) {
|
||||
registry.registerChatInputCommand(builder =>
|
||||
builder //
|
||||
.setName(this.name)
|
||||
.setDescription(this.description)
|
||||
);
|
||||
}
|
||||
|
||||
public override async chatInputRun(
|
||||
interaction: Command.ChatInputCommandInteraction
|
||||
) {
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle('About')
|
||||
.setDescription(
|
||||
'A Discord bot with slash commands, playlist support, Spotify, music quiz, saved playlists, lyrics, gifs and more.\n\n :white_small_square: [Commands](https://github.com/galnir/Master-Bot#commands)\n :white_small_square: [Contributors](https://github.com/galnir/Master-Bot#contributors-%EF%B8%8F)'
|
||||
)
|
||||
.setColor('Aqua');
|
||||
|
||||
return interaction.reply({ embeds: [embed] });
|
||||
}
|
||||
}
|
||||
74
apps/bot/src/commands/other/activity.ts
Normal file
74
apps/bot/src/commands/other/activity.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import { Command } from '@sapphire/framework';
|
||||
import { GuildMember, VoiceChannel } from 'discord.js';
|
||||
|
||||
@ApplyOptions<Command.Options>({
|
||||
name: 'activity',
|
||||
description: "Generate an invite link to your voice channel's activity",
|
||||
preconditions: ['isCommandDisabled', 'GuildOnly', 'inVoiceChannel']
|
||||
})
|
||||
export class ActivityCommand extends Command {
|
||||
public override registerApplicationCommands(registry: Command.Registry) {
|
||||
registry.registerChatInputCommand(builder =>
|
||||
builder //
|
||||
.setName(this.name)
|
||||
.setDescription(this.description)
|
||||
.addChannelOption(option =>
|
||||
option
|
||||
.setName('channel')
|
||||
.setDescription('Channel to invite to')
|
||||
.setRequired(true)
|
||||
)
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('activity')
|
||||
.setDescription('Activity to invite to')
|
||||
.setRequired(true)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public override async chatInputRun(
|
||||
interaction: Command.ChatInputCommandInteraction
|
||||
) {
|
||||
const channel = interaction.options.getChannel('channel', true);
|
||||
const activity = interaction.options.getString('activity', true);
|
||||
|
||||
if (
|
||||
channel.type.toString() !== 'GUILD_VOICE' ||
|
||||
channel.type.toString() === 'GUILD_CATEGORY'
|
||||
) {
|
||||
return interaction.reply({
|
||||
content: 'You can only invite to voice channels!'
|
||||
});
|
||||
}
|
||||
|
||||
const member = interaction.member as GuildMember;
|
||||
|
||||
if (!member) {
|
||||
return interaction.reply({
|
||||
content: 'You must be in a voice channel to use this command!'
|
||||
});
|
||||
}
|
||||
|
||||
if (member.voice.channelId !== channel.id) {
|
||||
return interaction.reply({
|
||||
content: 'You must be in the same voice channel to use this command!'
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const invite = await (channel as VoiceChannel).createInvite({
|
||||
reason: 'Activity invite'
|
||||
});
|
||||
|
||||
return interaction.reply({
|
||||
content: `[Click to join ${activity} in ${channel.name}](${invite.url})`
|
||||
});
|
||||
} catch {
|
||||
return interaction.reply({
|
||||
content: 'Something went wrong!'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
48
apps/bot/src/commands/other/advice.ts
Normal file
48
apps/bot/src/commands/other/advice.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import { Command } from '@sapphire/framework';
|
||||
import { EmbedBuilder } from 'discord.js';
|
||||
|
||||
@ApplyOptions<Command.Options>({
|
||||
name: 'advice',
|
||||
description: 'Get some advice!',
|
||||
preconditions: ['isCommandDisabled']
|
||||
})
|
||||
export class AdviceCommand extends Command {
|
||||
public override registerApplicationCommands(registry: Command.Registry) {
|
||||
registry.registerChatInputCommand(builder =>
|
||||
builder.setName(this.name).setDescription(this.description)
|
||||
);
|
||||
}
|
||||
|
||||
public override async chatInputRun(
|
||||
interaction: Command.ChatInputCommandInteraction
|
||||
) {
|
||||
try {
|
||||
const response = await fetch('https://api.adviceslip.com/advice');
|
||||
const data = await response.json();
|
||||
|
||||
const advice = data.slip?.advice;
|
||||
|
||||
if (!advice) {
|
||||
return interaction.reply({ content: 'Something went wrong!' });
|
||||
}
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor('NotQuiteBlack')
|
||||
.setAuthor({
|
||||
name: 'Advice Slip',
|
||||
url: 'https://adviceslip.com/',
|
||||
iconURL: 'https://i.imgur.com/8pIvnmD.png'
|
||||
})
|
||||
.setDescription(advice)
|
||||
.setTimestamp()
|
||||
.setFooter({
|
||||
text: `Powered by adviceslip.com`
|
||||
});
|
||||
|
||||
return interaction.reply({ embeds: [embed] });
|
||||
} catch {
|
||||
return interaction.reply({ content: 'Something went wrong!' });
|
||||
}
|
||||
}
|
||||
}
|
||||
36
apps/bot/src/commands/other/avatar.ts
Normal file
36
apps/bot/src/commands/other/avatar.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import { Command } from '@sapphire/framework';
|
||||
import { EmbedBuilder } from 'discord.js';
|
||||
|
||||
@ApplyOptions<Command.Options>({
|
||||
name: 'avatar',
|
||||
description: "Responds with a user's avatar",
|
||||
preconditions: ['isCommandDisabled', 'GuildOnly']
|
||||
})
|
||||
export class AvatarCommand extends Command {
|
||||
public override registerApplicationCommands(registry: Command.Registry) {
|
||||
registry.registerChatInputCommand(builder =>
|
||||
builder
|
||||
.setName(this.name)
|
||||
.setDescription(this.description)
|
||||
.addUserOption(option =>
|
||||
option
|
||||
.setName('user')
|
||||
.setDescription('The user to get the avatar of')
|
||||
.setRequired(true)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public override async chatInputRun(
|
||||
interaction: Command.ChatInputCommandInteraction
|
||||
) {
|
||||
const user = interaction.options.getUser('user', true);
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle(user.username)
|
||||
.setImage(user.displayAvatarURL({ size: 4096 }))
|
||||
.setColor('Aqua');
|
||||
|
||||
return interaction.reply({ embeds: [embed] });
|
||||
}
|
||||
}
|
||||
51
apps/bot/src/commands/other/chucknorris.ts
Normal file
51
apps/bot/src/commands/other/chucknorris.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import { Command } from '@sapphire/framework';
|
||||
import { EmbedBuilder } from 'discord.js';
|
||||
|
||||
@ApplyOptions<Command.Options>({
|
||||
name: 'chucknorris',
|
||||
description: 'Get a satirical fact about Chuck Norris!',
|
||||
preconditions: ['isCommandDisabled']
|
||||
})
|
||||
export class ChuckNorrisCommand extends Command {
|
||||
public override registerApplicationCommands(registry: Command.Registry) {
|
||||
registry.registerChatInputCommand(builder =>
|
||||
builder.setName(this.name).setDescription(this.description)
|
||||
);
|
||||
}
|
||||
|
||||
public override async chatInputRun(
|
||||
interaction: Command.ChatInputCommandInteraction
|
||||
) {
|
||||
try {
|
||||
const response = await fetch('https://api.chucknorris.io/jokes/random');
|
||||
const data = await response.json();
|
||||
|
||||
const joke = data;
|
||||
|
||||
if (!joke) {
|
||||
return interaction.reply({
|
||||
content: ':x: An error occured, Chuck is investigating this!'
|
||||
});
|
||||
}
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor('Orange')
|
||||
.setAuthor({
|
||||
name: 'Chuck Norris',
|
||||
url: 'https://chucknorris.io',
|
||||
iconURL: joke.icon_url
|
||||
})
|
||||
.setDescription(joke.value)
|
||||
.setTimestamp()
|
||||
.setFooter({
|
||||
text: 'Powered by chucknorris.io'
|
||||
});
|
||||
return interaction.reply({ embeds: [embed] });
|
||||
} catch {
|
||||
return interaction.reply({
|
||||
content: ':x: An error occured, Chuck is investigating this!'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
51
apps/bot/src/commands/other/fortune.ts
Normal file
51
apps/bot/src/commands/other/fortune.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import { Command } from '@sapphire/framework';
|
||||
import { EmbedBuilder } from 'discord.js';
|
||||
|
||||
@ApplyOptions<Command.Options>({
|
||||
name: 'fortune',
|
||||
description: 'Replies with a fortune cookie tip!',
|
||||
preconditions: ['isCommandDisabled']
|
||||
})
|
||||
export class FortuneCommand extends Command {
|
||||
public override registerApplicationCommands(registry: Command.Registry) {
|
||||
registry.registerChatInputCommand(builder =>
|
||||
builder.setName(this.name).setDescription(this.description)
|
||||
);
|
||||
}
|
||||
|
||||
public override async chatInputRun(
|
||||
interaction: Command.ChatInputCommandInteraction
|
||||
) {
|
||||
try {
|
||||
const response = await fetch('http://yerkee.com/api/fortune');
|
||||
const data = await response.json();
|
||||
|
||||
const tip = data.fortune;
|
||||
|
||||
if (!tip) {
|
||||
return interaction.reply({
|
||||
content: 'Something went wrong!'
|
||||
});
|
||||
}
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor('Orange')
|
||||
.setAuthor({
|
||||
name: 'Fortune Cookie',
|
||||
url: 'https://yerkee.com',
|
||||
iconURL: 'https://i.imgur.com/58wIjK0.png'
|
||||
})
|
||||
.setDescription(tip)
|
||||
.setTimestamp()
|
||||
.setFooter({
|
||||
text: 'Powered by yerkee.com'
|
||||
});
|
||||
return interaction.reply({ embeds: [embed] });
|
||||
} catch {
|
||||
return interaction.reply({
|
||||
content: 'Something went wrong!'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
234
apps/bot/src/commands/other/game-search.ts
Normal file
234
apps/bot/src/commands/other/game-search.ts
Normal file
@@ -0,0 +1,234 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import { Command } from '@sapphire/framework';
|
||||
import { PaginatedMessage } from '@sapphire/discord.js-utilities';
|
||||
import { env } from '../../env';
|
||||
import axios from 'axios';
|
||||
|
||||
@ApplyOptions<Command.Options>({
|
||||
name: 'game-search',
|
||||
description: 'Search for video game information',
|
||||
preconditions: ['isCommandDisabled']
|
||||
})
|
||||
export class ChuckNorrisCommand extends Command {
|
||||
public override registerApplicationCommands(registry: Command.Registry) {
|
||||
registry.registerChatInputCommand(builder =>
|
||||
builder
|
||||
.setName(this.name)
|
||||
.setDescription(this.description)
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('game')
|
||||
.setDescription('The game you want to look up?')
|
||||
.setRequired(true)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public override async chatInputRun(
|
||||
interaction: Command.ChatInputCommandInteraction
|
||||
) {
|
||||
if (!env.RAWG_API) {
|
||||
return interaction.reply({
|
||||
content: 'This command is disabled because the RAWG API key is not set.'
|
||||
});
|
||||
}
|
||||
|
||||
const title = interaction.options.getString('game', true);
|
||||
const filteredTitle = this.filterTitle(title);
|
||||
|
||||
const game = await this.getGameDetails(filteredTitle);
|
||||
|
||||
if (!game) {
|
||||
return interaction.reply({
|
||||
content: 'No game found with that name'
|
||||
});
|
||||
}
|
||||
|
||||
const PaginatedEmbed = new PaginatedMessage();
|
||||
|
||||
const firstPageTuple: string[] = []; // releaseDate, esrbRating, userRating
|
||||
|
||||
if (game.tba) {
|
||||
firstPageTuple.push('TBA');
|
||||
} else if (!game.released) {
|
||||
firstPageTuple.push('None Listed');
|
||||
} else {
|
||||
firstPageTuple.push(game.released);
|
||||
}
|
||||
|
||||
if (!game.esrb_rating) {
|
||||
firstPageTuple.push('None Listed');
|
||||
} else {
|
||||
firstPageTuple.push(game.esrb_rating.name);
|
||||
}
|
||||
|
||||
if (!game.rating) {
|
||||
firstPageTuple.push('None Listed');
|
||||
} else {
|
||||
firstPageTuple.push(game.rating + '/5');
|
||||
}
|
||||
|
||||
PaginatedEmbed.addPageEmbed(embed =>
|
||||
embed
|
||||
.setTitle(`Game Info: ${game.name}`)
|
||||
.setDescription(
|
||||
'>>> ' +
|
||||
'**Game Description**\n' +
|
||||
game.description_raw.slice(0, 2000) +
|
||||
'...'
|
||||
)
|
||||
.setColor('Grey')
|
||||
.setThumbnail(game.background_image)
|
||||
.addFields(
|
||||
{ name: 'Released', value: '> ' + firstPageTuple[0], inline: true },
|
||||
{
|
||||
name: 'ESRB Rating',
|
||||
value: '> ' + firstPageTuple[1],
|
||||
inline: true
|
||||
},
|
||||
{ name: 'Score', value: '> ' + firstPageTuple[2], inline: true }
|
||||
)
|
||||
.setTimestamp()
|
||||
);
|
||||
|
||||
const developerArray: string[] = [];
|
||||
if (game.developers.length) {
|
||||
for (let i = 0; i < game.developers.length; ++i) {
|
||||
developerArray.push(game.developers[i].name);
|
||||
}
|
||||
} else {
|
||||
developerArray.push('None Listed');
|
||||
}
|
||||
|
||||
const publisherArray: string[] = [];
|
||||
if (game.publishers.length) {
|
||||
for (let i = 0; i < game.publishers.length; ++i) {
|
||||
publisherArray.push(game.publishers[i].name);
|
||||
}
|
||||
} else {
|
||||
publisherArray.push('None Listed');
|
||||
}
|
||||
|
||||
const platformArray: string[] = [];
|
||||
if (game.platforms.length) {
|
||||
for (let i = 0; i < game.platforms.length; ++i) {
|
||||
platformArray.push(game.platforms[i].platform.name);
|
||||
}
|
||||
} else {
|
||||
platformArray.push('None Listed');
|
||||
}
|
||||
|
||||
const genreArray: string[] = [];
|
||||
if (game.genres.length) {
|
||||
for (let i = 0; i < game.genres.length; ++i) {
|
||||
genreArray.push(game.genres[i].name);
|
||||
}
|
||||
} else {
|
||||
genreArray.push('None Listed');
|
||||
}
|
||||
|
||||
const retailerArray: string[] = [];
|
||||
if (game.stores.length) {
|
||||
for (let i = 0; i < game.stores.length; ++i) {
|
||||
retailerArray.push(
|
||||
`[${game.stores[i].store.name}](${game.stores[i].url})`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
retailerArray.push('None Listed');
|
||||
}
|
||||
|
||||
PaginatedEmbed.addPageEmbed(embed =>
|
||||
embed
|
||||
.setTitle(`Game Info: ${game.name}`)
|
||||
.setColor('Grey')
|
||||
.setThumbnail(game.background_image_additional ?? game.background_image)
|
||||
// Row 1
|
||||
.addFields(
|
||||
{
|
||||
name: developerArray.length == 1 ? 'Developer' : 'Developers',
|
||||
value: '> ' + developerArray.toString().replace(/,/g, ', '),
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: publisherArray.length == 1 ? 'Publisher' : 'Publishers',
|
||||
value: '> ' + publisherArray.toString().replace(/,/g, ', '),
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: platformArray.length == 1 ? 'Platform' : 'Platforms',
|
||||
value: '> ' + platformArray.toString().replace(/,/g, ', '),
|
||||
inline: true
|
||||
}
|
||||
)
|
||||
// Row 2
|
||||
.addFields(
|
||||
{
|
||||
name: genreArray.length == 1 ? 'Genre' : 'Genres',
|
||||
value: '> ' + genreArray.toString().replace(/,/g, ', '),
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: retailerArray.length == 1 ? 'Retailer' : 'Retailers',
|
||||
value:
|
||||
'> ' +
|
||||
retailerArray.toString().replace(/,/g, ', ').replace(/`/g, '')
|
||||
}
|
||||
)
|
||||
.setTimestamp()
|
||||
);
|
||||
if (PaginatedEmbed.actions.size > 0)
|
||||
PaginatedEmbed.actions.delete('@sapphire/paginated-messages.goToPage');
|
||||
return PaginatedEmbed.run(interaction);
|
||||
}
|
||||
|
||||
private filterTitle(title: string) {
|
||||
return title.replace(/ /g, '-').replace(/' /g, '').toLowerCase();
|
||||
}
|
||||
|
||||
private getGameDetails(query: string): Promise<any> {
|
||||
return new Promise(async function (resolve, reject) {
|
||||
const url = `https://api.rawg.io/api/games/${encodeURIComponent(
|
||||
query
|
||||
)}?key=${env.RAWG_API}`;
|
||||
try {
|
||||
const response = await axios.get(url);
|
||||
if (response.status === 429) {
|
||||
reject(':x: Rate Limit exceeded. Please try again in a few minutes.');
|
||||
}
|
||||
if (response.status === 503) {
|
||||
reject(
|
||||
':x: The service is currently unavailable. Please try again later.'
|
||||
);
|
||||
}
|
||||
if (response.status === 404) {
|
||||
reject(`:x: Error: ${query} was not found`);
|
||||
}
|
||||
if (response.status !== 200) {
|
||||
reject(
|
||||
':x: There was a problem getting game from the API, make sure you entered a valid game tittle'
|
||||
);
|
||||
}
|
||||
|
||||
let body = response.data;
|
||||
if (body.redirect) {
|
||||
const redirect = await axios.get(
|
||||
`https://api.rawg.io/api/games/${body.slug}?key=${env.RAWG_API}`
|
||||
);
|
||||
body = redirect.data;
|
||||
}
|
||||
// 'id' is the only value that must be present to all valid queries
|
||||
if (!body.id) {
|
||||
reject(
|
||||
':x: There was a problem getting data from the API, make sure you entered a valid game title'
|
||||
);
|
||||
}
|
||||
resolve(body);
|
||||
} catch (e) {
|
||||
reject(
|
||||
'There was a problem getting data from the API, make sure you entered a valid game title'
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
161
apps/bot/src/commands/other/help.ts
Normal file
161
apps/bot/src/commands/other/help.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import {
|
||||
PaginatedMessage,
|
||||
PaginatedFieldMessageEmbed
|
||||
} from '@sapphire/discord.js-utilities';
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import { Command, CommandOptions, container } from '@sapphire/framework';
|
||||
import {
|
||||
ApplicationCommandOption,
|
||||
AutocompleteInteraction,
|
||||
EmbedBuilder
|
||||
} from 'discord.js';
|
||||
|
||||
@ApplyOptions<CommandOptions>({
|
||||
name: 'help',
|
||||
description: 'Get the Command List or add a command-name to get more info.',
|
||||
preconditions: ['isCommandDisabled']
|
||||
})
|
||||
export class HelpCommand extends Command {
|
||||
public override registerApplicationCommands(
|
||||
registry: Command.Registry
|
||||
): void {
|
||||
registry.registerChatInputCommand(builder =>
|
||||
builder
|
||||
.setName(this.name)
|
||||
.setDescription(this.description)
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('command-name')
|
||||
.setDescription('Which command would you like to know about?')
|
||||
.setRequired(false)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public override autocompleteRun(interaction: AutocompleteInteraction) {
|
||||
const commands = interaction.client.application?.commands.cache;
|
||||
const focusedOption = interaction.options.getFocused(true);
|
||||
const result = commands
|
||||
?.sorted((a, b) => a.name.localeCompare(b.name))
|
||||
.filter(choice => choice.name.startsWith(focusedOption.value.toString()))
|
||||
.map(choice => ({ name: choice.name, value: choice.name }))
|
||||
.slice(0, 10);
|
||||
interaction;
|
||||
return interaction.respond(result!);
|
||||
}
|
||||
public override async chatInputRun(
|
||||
interaction: Command.ChatInputCommandInteraction
|
||||
) {
|
||||
const { client } = container;
|
||||
|
||||
const query = interaction.options.getString('command-name')?.toLowerCase();
|
||||
const array: CommandInfo[] = [];
|
||||
|
||||
const app = client.application;
|
||||
app?.commands.cache.each(command => {
|
||||
array.push({
|
||||
name: command.name,
|
||||
options: command.options,
|
||||
details: command.description
|
||||
});
|
||||
});
|
||||
|
||||
// Sort the array by name
|
||||
const sortedList = array.sort((a, b) => {
|
||||
let fa = a.name.toLowerCase(),
|
||||
fb = b.name.toLowerCase();
|
||||
|
||||
if (fa < fb) {
|
||||
return -1;
|
||||
}
|
||||
if (fa > fb) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
if (!query) {
|
||||
let characters = 0;
|
||||
let page = 0;
|
||||
let message: string[] = [];
|
||||
const PaginatedEmbed = new PaginatedMessage();
|
||||
sortedList.forEach((command, index) => {
|
||||
characters += command.details.length + command.details.length;
|
||||
message.push(`> **/${command.name}** - ${command.details}\n`);
|
||||
|
||||
if (characters > 1500 || index == sortedList.length - 1) {
|
||||
page++;
|
||||
characters = 0;
|
||||
PaginatedEmbed.addPageEmbed(
|
||||
new EmbedBuilder()
|
||||
.setTitle(`Command List - Page ${page}`)
|
||||
.setThumbnail(app?.iconURL()!)
|
||||
.setColor('Purple')
|
||||
.setAuthor({
|
||||
name: interaction.user.username + ' - Help Command',
|
||||
iconURL: interaction.user.displayAvatarURL()
|
||||
})
|
||||
.setDescription(message.toString().replaceAll(',> **/', '> **/'))
|
||||
);
|
||||
message = [];
|
||||
}
|
||||
});
|
||||
|
||||
return PaginatedEmbed.run(interaction);
|
||||
} else {
|
||||
const commandMap = new Map();
|
||||
sortedList.reduce(
|
||||
(obj, command) => commandMap.set(command.name, command),
|
||||
{}
|
||||
);
|
||||
if (commandMap.has(query)) {
|
||||
const command: CommandInfo = commandMap.get(query);
|
||||
const optionsList: any[] = [];
|
||||
command.options.forEach(option => {
|
||||
optionsList.push({
|
||||
name: option.name,
|
||||
description: option.description
|
||||
});
|
||||
});
|
||||
const DetailedPagination = new PaginatedFieldMessageEmbed();
|
||||
|
||||
const commandDetails = new EmbedBuilder()
|
||||
.setAuthor({
|
||||
name: interaction.user.username + ' - Help Command',
|
||||
iconURL: interaction.user.displayAvatarURL()
|
||||
})
|
||||
.setThumbnail(app?.iconURL()!)
|
||||
.setTitle(
|
||||
`${
|
||||
command.name.charAt(0).toUpperCase() +
|
||||
command.name.slice(1).toLowerCase()
|
||||
} - Details`
|
||||
)
|
||||
.setColor('Purple')
|
||||
.setDescription(`**Description**\n> ${command.details}`);
|
||||
|
||||
if (!command.options.length)
|
||||
return await interaction.reply({ embeds: [commandDetails] });
|
||||
|
||||
DetailedPagination.setTemplate(commandDetails)
|
||||
.setTitleField('Options')
|
||||
.setItems(command.options)
|
||||
.formatItems(
|
||||
(option: any) => `**${option.name}**\n> ${option.description}`
|
||||
)
|
||||
.setItemsPerPage(5)
|
||||
.make();
|
||||
|
||||
return DetailedPagination.run(interaction);
|
||||
} else
|
||||
return await interaction.reply(
|
||||
`:x: Command: **${query}** was not found`
|
||||
);
|
||||
}
|
||||
interface CommandInfo {
|
||||
name: string;
|
||||
options: ApplicationCommandOption[];
|
||||
details: string;
|
||||
}
|
||||
}
|
||||
}
|
||||
52
apps/bot/src/commands/other/insult.ts
Normal file
52
apps/bot/src/commands/other/insult.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import { Command, CommandOptions } from '@sapphire/framework';
|
||||
import { EmbedBuilder } from 'discord.js';
|
||||
|
||||
@ApplyOptions<CommandOptions>({
|
||||
name: 'insult',
|
||||
description: 'Replies with a mean insult',
|
||||
preconditions: ['isCommandDisabled']
|
||||
})
|
||||
export class InsultCommand extends Command {
|
||||
public override registerApplicationCommands(
|
||||
registry: Command.Registry
|
||||
): void {
|
||||
registry.registerChatInputCommand({
|
||||
name: this.name,
|
||||
description: this.description
|
||||
});
|
||||
}
|
||||
|
||||
public override async chatInputRun(
|
||||
interaction: Command.ChatInputCommandInteraction
|
||||
) {
|
||||
try {
|
||||
const response = await fetch(
|
||||
'https://evilinsult.com/generate_insult.php?lang=en&type=json'
|
||||
);
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.insult)
|
||||
return interaction.reply({ content: 'Something went wrong!' });
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor('Red')
|
||||
.setAuthor({
|
||||
name: 'Evil Insult',
|
||||
url: 'https://evilinsult.com',
|
||||
iconURL: 'https://i.imgur.com/bOVpNAX.png'
|
||||
})
|
||||
.setDescription(data.insult)
|
||||
.setTimestamp()
|
||||
.setFooter({
|
||||
text: 'Powered by evilinsult.com'
|
||||
});
|
||||
|
||||
return interaction.reply({ embeds: [embed] });
|
||||
} catch {
|
||||
return interaction.reply({
|
||||
content: 'Something went wrong!'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
50
apps/bot/src/commands/other/kanye.ts
Normal file
50
apps/bot/src/commands/other/kanye.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import { Command, CommandOptions } from '@sapphire/framework';
|
||||
import { EmbedBuilder } from 'discord.js';
|
||||
|
||||
@ApplyOptions<CommandOptions>({
|
||||
name: 'kanye',
|
||||
description: 'Replies with a random Kanye quote',
|
||||
preconditions: ['isCommandDisabled']
|
||||
})
|
||||
export class KanyeCommand extends Command {
|
||||
public override async chatInputRun(
|
||||
interaction: Command.ChatInputCommandInteraction
|
||||
) {
|
||||
try {
|
||||
const response = await fetch('https://api.kanye.rest/?format=json');
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.quote)
|
||||
return interaction.reply({ content: 'Something went wrong!' });
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor('Orange')
|
||||
.setAuthor({
|
||||
name: 'Kanye Omari West',
|
||||
url: 'https://kanye.rest',
|
||||
iconURL: 'https://i.imgur.com/SsNoHVh.png'
|
||||
})
|
||||
.setDescription(data.quote)
|
||||
.setTimestamp()
|
||||
.setFooter({
|
||||
text: 'Powered by kanye.rest'
|
||||
});
|
||||
|
||||
return interaction.reply({ embeds: [embed] });
|
||||
} catch {
|
||||
return interaction.reply({
|
||||
content: 'Something went wrong!'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public override registerApplicationCommands(
|
||||
registry: Command.Registry
|
||||
): void {
|
||||
registry.registerChatInputCommand({
|
||||
name: this.name,
|
||||
description: this.description
|
||||
});
|
||||
}
|
||||
}
|
||||
51
apps/bot/src/commands/other/motivation.ts
Normal file
51
apps/bot/src/commands/other/motivation.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import { Command, CommandOptions } from '@sapphire/framework';
|
||||
import { EmbedBuilder } from 'discord.js';
|
||||
|
||||
@ApplyOptions<CommandOptions>({
|
||||
name: 'motivation',
|
||||
description: 'Replies with a motivational quote!',
|
||||
preconditions: ['isCommandDisabled']
|
||||
})
|
||||
export class MotivationCommand extends Command {
|
||||
public override registerApplicationCommands(
|
||||
registry: Command.Registry
|
||||
): void {
|
||||
registry.registerChatInputCommand({
|
||||
name: this.name,
|
||||
description: this.description
|
||||
});
|
||||
}
|
||||
public override async chatInputRun(
|
||||
interaction: Command.ChatInputCommandInteraction
|
||||
) {
|
||||
try {
|
||||
const response = await fetch('https://type.fit/api/quotes');
|
||||
const data = await response.json();
|
||||
|
||||
if (!data)
|
||||
return await interaction.reply({ content: 'Something went wrong!' });
|
||||
|
||||
const randomQuote = data[Math.floor(Math.random() * data.length)];
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor('Yellow')
|
||||
.setAuthor({
|
||||
name: 'Motivational Quote',
|
||||
url: 'https://type.fit',
|
||||
iconURL: 'https://i.imgur.com/Cnr6cQb.png'
|
||||
})
|
||||
.setDescription(`*"${randomQuote.text}*"\n\n-${randomQuote.author}`)
|
||||
.setTimestamp()
|
||||
.setFooter({
|
||||
text: 'Powered by type.fit'
|
||||
});
|
||||
|
||||
return await interaction.reply({ embeds: [embed] });
|
||||
} catch {
|
||||
return await interaction.reply({
|
||||
content: 'Something went wrong!'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
21
apps/bot/src/commands/other/ping.ts
Normal file
21
apps/bot/src/commands/other/ping.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import { Command } from '@sapphire/framework';
|
||||
|
||||
@ApplyOptions<Command.Options>({
|
||||
name: 'ping',
|
||||
description: 'Replies with pong!',
|
||||
preconditions: ['isCommandDisabled']
|
||||
})
|
||||
export class PingCommand extends Command {
|
||||
public override registerApplicationCommands(registry: Command.Registry) {
|
||||
registry.registerChatInputCommand(builder =>
|
||||
builder.setName(this.name).setDescription(this.description)
|
||||
);
|
||||
}
|
||||
|
||||
public override async chatInputRun(
|
||||
interaction: Command.ChatInputCommandInteraction
|
||||
) {
|
||||
return interaction.reply({ content: 'Pong!' });
|
||||
}
|
||||
}
|
||||
45
apps/bot/src/commands/other/random.ts
Normal file
45
apps/bot/src/commands/other/random.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import { Command, CommandOptions } from '@sapphire/framework';
|
||||
import { EmbedBuilder } from 'discord.js';
|
||||
|
||||
@ApplyOptions<CommandOptions>({
|
||||
name: 'random',
|
||||
description: 'Generate a random number between two inputs!',
|
||||
preconditions: ['isCommandDisabled']
|
||||
})
|
||||
export class RandomCommand extends Command {
|
||||
public override registerApplicationCommands(
|
||||
registry: Command.Registry
|
||||
): void {
|
||||
registry.registerChatInputCommand(builder =>
|
||||
builder
|
||||
.setName(this.name)
|
||||
.setDescription(this.description)
|
||||
.addIntegerOption(option =>
|
||||
option
|
||||
.setName('min')
|
||||
.setDescription('What is the minimum number?')
|
||||
.setRequired(true)
|
||||
)
|
||||
.addIntegerOption(option =>
|
||||
option
|
||||
.setName('max')
|
||||
.setDescription('What is the maximum number?')
|
||||
.setRequired(true)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public override async chatInputRun(
|
||||
interaction: Command.ChatInputCommandInteraction
|
||||
) {
|
||||
const min = Math.ceil(interaction.options.getInteger('min', true));
|
||||
const max = Math.floor(interaction.options.getInteger('max', true));
|
||||
|
||||
const rngEmbed = new EmbedBuilder().setTitle(
|
||||
`${Math.floor(Math.random() * (max - min + 1)) + min}`
|
||||
);
|
||||
|
||||
return await interaction.reply({ embeds: [rngEmbed] });
|
||||
}
|
||||
}
|
||||
215
apps/bot/src/commands/other/reddit.ts
Normal file
215
apps/bot/src/commands/other/reddit.ts
Normal file
@@ -0,0 +1,215 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import { Command, CommandOptions } from '@sapphire/framework';
|
||||
import {
|
||||
ColorResolvable,
|
||||
StringSelectMenuBuilder,
|
||||
ComponentType
|
||||
} from 'discord.js';
|
||||
import { PaginatedMessage } from '@sapphire/discord.js-utilities';
|
||||
import axios from 'axios';
|
||||
import Logger from '../../lib/logger';
|
||||
|
||||
@ApplyOptions<CommandOptions>({
|
||||
name: 'reddit',
|
||||
description: 'Get posts from reddit by specifying a subreddit',
|
||||
preconditions: ['GuildOnly', 'isCommandDisabled']
|
||||
})
|
||||
export class RedditCommand extends Command {
|
||||
public override registerApplicationCommands(
|
||||
registry: Command.Registry
|
||||
): void {
|
||||
registry.registerChatInputCommand(builder =>
|
||||
builder
|
||||
.setName(this.name)
|
||||
.setDescription(this.description)
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('subreddit')
|
||||
.setDescription('Subreddit name')
|
||||
.setRequired(true)
|
||||
)
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('sort')
|
||||
.setDescription(
|
||||
'What posts do you want to see? Select from best/hot/top/new/controversial/rising'
|
||||
)
|
||||
.setRequired(true)
|
||||
.addChoices(
|
||||
{
|
||||
name: 'Best',
|
||||
value: 'best'
|
||||
},
|
||||
{
|
||||
name: 'Hot',
|
||||
value: 'hot'
|
||||
},
|
||||
{
|
||||
name: 'New',
|
||||
value: 'new'
|
||||
},
|
||||
{
|
||||
name: 'Top',
|
||||
value: 'top'
|
||||
},
|
||||
{
|
||||
name: 'Controversial',
|
||||
value: 'controversial'
|
||||
},
|
||||
{
|
||||
name: 'Rising',
|
||||
value: 'rising'
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public override async chatInputRun(
|
||||
interaction: Command.ChatInputCommandInteraction
|
||||
) {
|
||||
await interaction.deferReply();
|
||||
const channel = interaction.channel;
|
||||
if (!channel) return await interaction.reply('Something went wrong :('); // type guard
|
||||
const subreddit = interaction.options.getString('subreddit', true);
|
||||
const sort = interaction.options.getString('sort', true);
|
||||
|
||||
if (['controversial', 'top'].some(val => val === sort)) {
|
||||
const row = new StringSelectMenuBuilder()
|
||||
.setCustomId('top_or_controversial')
|
||||
.setPlaceholder('Please select an option')
|
||||
.addOptions(optionsArray);
|
||||
|
||||
const menu = await channel.send({
|
||||
content: `:loud_sound: Do you want to get the ${sort} posts from past hour/week/month/year or all?`,
|
||||
components: [
|
||||
{
|
||||
type: ComponentType.ActionRow,
|
||||
|
||||
components: [row]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const collector = menu.createMessageComponentCollector({
|
||||
componentType: ComponentType.StringSelect,
|
||||
time: 30000 // 30 sec
|
||||
});
|
||||
|
||||
collector.on('end', () => {
|
||||
if (menu) menu.delete().catch(Logger.error);
|
||||
});
|
||||
|
||||
collector.on('collect', async i => {
|
||||
if (i.user.id !== interaction.user.id) {
|
||||
i.reply({
|
||||
content: 'This element is not for you!',
|
||||
ephemeral: true
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
collector.stop();
|
||||
const timeFilter = i.values[0];
|
||||
this.fetchFromReddit(interaction, subreddit, sort, timeFilter);
|
||||
return;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.fetchFromReddit(interaction, subreddit, sort);
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
private async fetchFromReddit(
|
||||
interaction: Command.ChatInputCommandInteraction,
|
||||
subreddit: string,
|
||||
sort: string,
|
||||
timeFilter = 'day'
|
||||
) {
|
||||
try {
|
||||
var data = await this.getData(subreddit, sort, timeFilter);
|
||||
} catch (error: any) {
|
||||
return interaction.followUp(error);
|
||||
}
|
||||
|
||||
// interaction.followUp('Fetching data from reddit');
|
||||
|
||||
const paginatedEmbed = new PaginatedMessage();
|
||||
for (let i = 1; i <= data.children.length; i++) {
|
||||
let color: ColorResolvable = 'Orange';
|
||||
let redditPost = data.children[i - 1];
|
||||
|
||||
if (redditPost.data.title.length > 255) {
|
||||
redditPost.data.title = redditPost.data.title.substring(0, 252) + '...'; // max title length is 256
|
||||
}
|
||||
|
||||
if (redditPost.data.selftext.length > 1024) {
|
||||
redditPost.data.selftext =
|
||||
redditPost.data.selftext.substring(0, 1024) +
|
||||
`[Read More...](https://www.reddit.com${redditPost.data.permalink})`;
|
||||
}
|
||||
|
||||
if (redditPost.data.over_18) color = 'Red'; // red - nsfw
|
||||
|
||||
paginatedEmbed.addPageEmbed(embed =>
|
||||
embed
|
||||
.setColor(color)
|
||||
.setTitle(redditPost.data.title)
|
||||
.setURL(`https://www.reddit.com${redditPost.data.permalink}`)
|
||||
.setDescription(
|
||||
`${
|
||||
redditPost.data.over_18 ? '' : redditPost.data.selftext + '\n\n'
|
||||
}Upvotes: ${redditPost.data.score} :thumbsup: `
|
||||
)
|
||||
.setAuthor({ name: redditPost.data.author })
|
||||
);
|
||||
}
|
||||
|
||||
return paginatedEmbed.run(interaction);
|
||||
}
|
||||
|
||||
private getData(
|
||||
subreddit: string,
|
||||
sort: string,
|
||||
timeFilter: string
|
||||
): Promise<any> {
|
||||
return new Promise(async function (resolve, reject) {
|
||||
const response = await axios.get(
|
||||
`https://www.reddit.com/r/${subreddit}/${sort}/.json?limit=10&t=${
|
||||
timeFilter ? timeFilter : 'day'
|
||||
}`
|
||||
);
|
||||
const data = response.data.data;
|
||||
if (!data) {
|
||||
reject(`**${subreddit}** is a private subreddit!`);
|
||||
} else if (!data.children.length) {
|
||||
reject('Please provide a valid subreddit name!');
|
||||
}
|
||||
resolve(data);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const optionsArray = [
|
||||
{
|
||||
label: 'hour',
|
||||
value: 'hour'
|
||||
},
|
||||
{
|
||||
label: 'week',
|
||||
value: 'week'
|
||||
},
|
||||
{
|
||||
label: 'month',
|
||||
value: 'month'
|
||||
},
|
||||
{
|
||||
label: 'year',
|
||||
value: 'year'
|
||||
},
|
||||
{
|
||||
label: 'all',
|
||||
value: 'all'
|
||||
}
|
||||
];
|
||||
80
apps/bot/src/commands/other/rockpaperscissors.ts
Normal file
80
apps/bot/src/commands/other/rockpaperscissors.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import { Command, CommandOptions } from '@sapphire/framework';
|
||||
import { Colors, EmbedBuilder } from 'discord.js';
|
||||
|
||||
@ApplyOptions<CommandOptions>({
|
||||
name: 'rockpaperscissors',
|
||||
description: 'Play rock paper scissors with me!',
|
||||
preconditions: ['isCommandDisabled']
|
||||
})
|
||||
export class RockPaperScissorsCommand extends Command {
|
||||
public override registerApplicationCommands(
|
||||
registry: Command.Registry
|
||||
): void {
|
||||
registry.registerChatInputCommand(builder =>
|
||||
builder
|
||||
.setName(this.name)
|
||||
.setDescription(this.description)
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('move')
|
||||
.setDescription('What is your move?')
|
||||
.setRequired(true)
|
||||
.addChoices(
|
||||
{ name: 'Rock', value: 'rock' },
|
||||
{ name: 'Paper', value: 'paper' },
|
||||
{ name: 'Scissors', value: 'scissors' }
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public override async chatInputRun(
|
||||
interaction: Command.ChatInputCommandInteraction
|
||||
) {
|
||||
const move = interaction.options.getString('move', true) as
|
||||
| 'rock'
|
||||
| 'paper'
|
||||
| 'scissors';
|
||||
const resultMessage = this.rpsLogic(move);
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(Colors.White)
|
||||
.setTitle('Rock, Paper, Scissors')
|
||||
.setDescription(`**${resultMessage[0]}**, I formed ${resultMessage[1]}`);
|
||||
|
||||
return await interaction.reply({ embeds: [embed] });
|
||||
}
|
||||
|
||||
private rpsLogic(player_move: string) {
|
||||
const bot_move = ['rock', 'paper', 'scissors'][
|
||||
Math.floor(Math.random() * 3)
|
||||
];
|
||||
|
||||
if (player_move === 'rock') {
|
||||
if (bot_move === 'rock') {
|
||||
return ['Tie!', 'Rock'];
|
||||
}
|
||||
if (bot_move === 'paper') {
|
||||
return ['I win!', 'Paper'];
|
||||
}
|
||||
return ['You win!', 'Scissors'];
|
||||
} else if (player_move === 'paper') {
|
||||
if (bot_move === 'rock') {
|
||||
return ['You win!', 'Rock'];
|
||||
}
|
||||
if (bot_move === 'paper') {
|
||||
return ['Tie!', 'Paper'];
|
||||
}
|
||||
return ['I win!', 'Scissors'];
|
||||
} else {
|
||||
if (bot_move === 'rock') {
|
||||
return ['I win!', 'Rock'];
|
||||
}
|
||||
if (bot_move === 'paper') {
|
||||
return ['You win!', 'Paper'];
|
||||
}
|
||||
return ['Tie!', 'Scissors'];
|
||||
}
|
||||
}
|
||||
}
|
||||
342
apps/bot/src/commands/other/speedrun.ts
Normal file
342
apps/bot/src/commands/other/speedrun.ts
Normal file
@@ -0,0 +1,342 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import { PaginatedMessage } from '@sapphire/discord.js-utilities';
|
||||
import { Command, CommandOptions } from '@sapphire/framework';
|
||||
import axios from 'axios';
|
||||
import { EmbedBuilder, Colors, ButtonStyle, ComponentType } from 'discord.js';
|
||||
import Logger from '../../lib/logger';
|
||||
|
||||
@ApplyOptions<CommandOptions>({
|
||||
name: 'speedrun',
|
||||
description: 'Look for the world record of a game!',
|
||||
preconditions: ['isCommandDisabled']
|
||||
})
|
||||
export class SpeedRunCommand extends Command {
|
||||
public override registerApplicationCommands(
|
||||
registry: Command.Registry
|
||||
): void {
|
||||
registry.registerChatInputCommand(builder =>
|
||||
builder
|
||||
.setName(this.name)
|
||||
.setDescription(this.description)
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('game')
|
||||
.setDescription('Video Game Title?')
|
||||
.setRequired(true)
|
||||
)
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('category')
|
||||
.setDescription('speed run Category?')
|
||||
.setRequired(false)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public override async chatInputRun(
|
||||
interaction: Command.ChatInputCommandInteraction
|
||||
) {
|
||||
const query = interaction.options.getString('game', true);
|
||||
let queryCat = interaction.options.getString('category', false);
|
||||
|
||||
let initialRaw;
|
||||
|
||||
try {
|
||||
initialRaw = await axios.get(
|
||||
`https://www.speedrun.com/api/v1/games?name=${query}`
|
||||
);
|
||||
} catch {
|
||||
return interaction.reply({
|
||||
content: 'Something went wrong, please try again later'
|
||||
});
|
||||
}
|
||||
const initial = initialRaw.data;
|
||||
|
||||
if (!initial.data.length) {
|
||||
return interaction.reply({ content: 'No game was found.' });
|
||||
}
|
||||
|
||||
let gameID: string = initial.data[0].id;
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await axios.get(
|
||||
`https://www.speedrun.com/api/v1/games/${gameID}/records?miscellaneous=no&scope=full-game&top=10&embed=game,category,players,platforms,regions`
|
||||
);
|
||||
} catch (error) {
|
||||
Logger.error(`${this.name} Command - ${JSON.stringify(error)}`);
|
||||
return interaction.reply({
|
||||
content: 'Something went wrong, please try again later'
|
||||
});
|
||||
}
|
||||
const body = response.data;
|
||||
|
||||
if (!body.data.length) {
|
||||
const gameNameArr: string[] = [];
|
||||
initial.data.slice(0, 6).forEach((id: any) => {
|
||||
gameNameArr.push(id.names.international);
|
||||
});
|
||||
let gameName = new EmbedBuilder()
|
||||
.setColor('Green')
|
||||
.setTitle(':mag: Search Results')
|
||||
.setThumbnail(initial.data[0].assets['cover-medium'].uri)
|
||||
.addFields({
|
||||
name: ':x: Try searching again with the following suggestions.',
|
||||
value: initial.data[0].names.international + ` doesn't have any runs.`
|
||||
})
|
||||
.setTimestamp()
|
||||
.setFooter({ text: 'Powered by www.speedrun.com' });
|
||||
|
||||
gameNameArr.forEach((game, i) => {
|
||||
gameName.addFields({
|
||||
name: `:video_game: Result ${i + 1}`,
|
||||
value: game
|
||||
});
|
||||
});
|
||||
|
||||
interaction.reply({ embeds: [gameName] });
|
||||
} else {
|
||||
const categories = body.data;
|
||||
queryCat = !queryCat ? categories[0].category.data.name : queryCat;
|
||||
for (let i = 0; i <= categories.length; ++i) {
|
||||
if (
|
||||
categories[i]?.category.data.name.toLowerCase() ==
|
||||
queryCat?.toLowerCase()
|
||||
) {
|
||||
break;
|
||||
} else if (i == categories.length)
|
||||
queryCat = categories[0].category.data.name;
|
||||
}
|
||||
return await interaction
|
||||
.reply({
|
||||
embeds: [
|
||||
new EmbedBuilder().setColor('Green').setDescription('Getting Data')
|
||||
],
|
||||
fetchReply: true
|
||||
})
|
||||
.then(async () => {
|
||||
SpeedRunCommand.embedGenerator(
|
||||
categories,
|
||||
queryCat ?? categories[0].category.data.name
|
||||
)
|
||||
.setIdle(30 * 1000)
|
||||
.setIndex(0)
|
||||
.run(interaction);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static myButtons(
|
||||
message: PaginatedMessage,
|
||||
categories: any,
|
||||
queryCat: string
|
||||
) {
|
||||
categories.forEach((value: null, index: number) => {
|
||||
message.addAction({
|
||||
style:
|
||||
categories[index].category.data.name.toLowerCase() ==
|
||||
queryCat.toLowerCase()
|
||||
? ButtonStyle.Success
|
||||
: ButtonStyle.Primary,
|
||||
customId: `Category-${index}`,
|
||||
label: categories[index].category.data.name,
|
||||
type: ComponentType.Button,
|
||||
run: async ({ interaction }) => {
|
||||
// message = new PaginatedMessage();
|
||||
queryCat = categories[index].category.data.name;
|
||||
message = SpeedRunCommand.embedGenerator(categories, queryCat);
|
||||
try {
|
||||
SpeedRunCommand.myButtons(
|
||||
message.setIndex(0).setIdle(30 * 1000),
|
||||
categories,
|
||||
queryCat
|
||||
);
|
||||
} catch (error: any) {
|
||||
new PaginatedMessage()
|
||||
.addPageEmbed(
|
||||
new EmbedBuilder()
|
||||
.setColor(Colors.Red)
|
||||
.setTitle('Error')
|
||||
.setDescription(error.toString())
|
||||
)
|
||||
.run(interaction);
|
||||
}
|
||||
await interaction
|
||||
.update({
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setColor('Green')
|
||||
.setDescription('Getting Data')
|
||||
],
|
||||
fetchReply: true
|
||||
})
|
||||
.then(async () => {
|
||||
message.run(interaction);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
return message;
|
||||
}
|
||||
static embedGenerator(categories: any, queryCat: string) {
|
||||
const PaginatedEmbed = new PaginatedMessage();
|
||||
try {
|
||||
categories.forEach((category: any) => {
|
||||
if (
|
||||
category.category.data.name.toLowerCase() == queryCat?.toLowerCase()
|
||||
) {
|
||||
const catRules = new EmbedBuilder()
|
||||
.setDescription(
|
||||
category.category.data.rules.toString().length
|
||||
? `**${category.category.data.name} Rules**:\n` +
|
||||
category.category.data.rules.toString()
|
||||
: 'No Data'
|
||||
)
|
||||
.setColor('Green')
|
||||
.setThumbnail(category.game.data.assets['cover-medium'].uri)
|
||||
.setAuthor({
|
||||
name:
|
||||
category.game.data.names.international +
|
||||
' - ' +
|
||||
category.category.data.name,
|
||||
url: 'http://speedrun.com/'
|
||||
});
|
||||
PaginatedEmbed.addPageEmbed(catRules);
|
||||
for (let i = 0; i <= category.players.data.length; ++i) {
|
||||
const platform: string =
|
||||
category.platforms.data.length > 0
|
||||
? category.platforms.data[0].name
|
||||
: '';
|
||||
const region: string =
|
||||
category.regions.data.length > 0
|
||||
? ' - ' + category.regions.data[0].name
|
||||
: '';
|
||||
let emu: string = 'No Data';
|
||||
let runnerName: string = 'No Data';
|
||||
let trophyIcon: string = '';
|
||||
if (category.runs[i]) {
|
||||
emu = category.runs[i].run.system.emulated ? ' [EMU]' : '';
|
||||
runnerName =
|
||||
category.players.data[i].rel === 'user'
|
||||
? category.players.data[i].names.international
|
||||
: category.players.data[i].name;
|
||||
|
||||
if (i == 0) trophyIcon = '🏆 WR: ';
|
||||
if (i == 1) trophyIcon = '🥈 2nd: ';
|
||||
if (i == 2) trophyIcon = '🥉 3rd: ';
|
||||
if (i >= 3) trophyIcon = `${i + 1}th: `;
|
||||
}
|
||||
if (category.runs[i]) {
|
||||
PaginatedEmbed.addPageEmbed(embeds =>
|
||||
embeds
|
||||
.setColor('Green')
|
||||
.setTitle(
|
||||
category.runs[i]
|
||||
? trophyIcon +
|
||||
SpeedRunCommand.convertTime(
|
||||
category.runs[i].run.times.primary_t
|
||||
) +
|
||||
' by ' +
|
||||
runnerName
|
||||
: 'No Data'
|
||||
)
|
||||
.setThumbnail(category.game.data.assets['cover-medium'].uri)
|
||||
.setURL(
|
||||
category.runs[i]
|
||||
? category.runs[i].run.weblink
|
||||
: category.weblink
|
||||
)
|
||||
.setAuthor({
|
||||
name:
|
||||
category.game.data.names.international +
|
||||
' - ' +
|
||||
category.category.data.name,
|
||||
url: 'http://speedrun.com/'
|
||||
})
|
||||
.addFields(
|
||||
{
|
||||
name: ':calendar_spiral: Date Played:',
|
||||
value: category.runs[i]
|
||||
? category.runs[i].run.date
|
||||
: 'No Data'
|
||||
},
|
||||
{
|
||||
name: ':video_game: Played On:',
|
||||
value: platform + region + emu
|
||||
}
|
||||
)
|
||||
.setFooter({
|
||||
text: 'Powered by www.speedrun.com',
|
||||
iconURL: 'https://i.imgur.com/PpxR9E1.png'
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
PaginatedEmbed.setIdle(30 * 1000).setIndex(0);
|
||||
if (PaginatedEmbed.actions.size > 0)
|
||||
PaginatedEmbed.actions.delete('@sapphire/paginated-messages.goToPage');
|
||||
|
||||
SpeedRunCommand.myButtons(PaginatedEmbed, categories, queryCat);
|
||||
|
||||
return PaginatedEmbed;
|
||||
} catch (error: any) {
|
||||
Logger.error(`${this.name} Command - ${JSON.stringify(error)}`);
|
||||
return new PaginatedMessage().addPageEmbed(
|
||||
new EmbedBuilder()
|
||||
.setColor(Colors.Red)
|
||||
.setTitle('Error')
|
||||
.setDescription(error.toString())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static convertTime(time: number) {
|
||||
let str, hr, min: any, sec, ms: number | string | undefined;
|
||||
let parts = time.toString().split('.');
|
||||
ms = parts.length > 1 ? parseInt((parts[1] + '00').slice(0, 3)) : undefined;
|
||||
sec = parseInt(parts[0]);
|
||||
if (sec >= 60) {
|
||||
min = Math.floor(sec / 60);
|
||||
sec = sec % 60;
|
||||
sec = sec < 10 ? '0' + sec : sec;
|
||||
}
|
||||
if (min >= 60) {
|
||||
hr = Math.floor(min / 60);
|
||||
min = min % 60;
|
||||
min = min < 10 ? '0' + min : min;
|
||||
}
|
||||
if (ms && ms < 10) ms = '00' + ms;
|
||||
else if (ms && ms < 100) ms = '0' + ms;
|
||||
if (min == undefined) {
|
||||
str =
|
||||
ms == undefined
|
||||
? sec.toString() + 's'
|
||||
: sec.toString() + 's ' + ms.toString() + 'ms';
|
||||
} else if (hr === undefined) {
|
||||
str =
|
||||
ms === undefined
|
||||
? min.toString() + 'm ' + sec.toString() + 's'
|
||||
: min.toString() +
|
||||
'm ' +
|
||||
sec.toString() +
|
||||
's ' +
|
||||
ms.toString() +
|
||||
'ms';
|
||||
} else {
|
||||
str =
|
||||
ms === undefined
|
||||
? hr.toString() + 'h ' + min.toString() + 'm ' + sec.toString() + 's'
|
||||
: hr.toString() +
|
||||
'h ' +
|
||||
min.toString() +
|
||||
'm ' +
|
||||
sec.toString() +
|
||||
's ' +
|
||||
ms.toString() +
|
||||
'ms';
|
||||
}
|
||||
return str;
|
||||
}
|
||||
}
|
||||
68
apps/bot/src/commands/other/translate.ts
Normal file
68
apps/bot/src/commands/other/translate.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import { Command, CommandOptions } from '@sapphire/framework';
|
||||
import axios from 'axios';
|
||||
import { EmbedBuilder } from 'discord.js';
|
||||
import translate from 'google-translate-api-x';
|
||||
import Logger from '../../lib/logger';
|
||||
@ApplyOptions<CommandOptions>({
|
||||
name: 'translate',
|
||||
description:
|
||||
'Translate from any language to any language using Google Translate',
|
||||
preconditions: ['GuildOnly', 'isCommandDisabled', 'validateLanguageCode']
|
||||
})
|
||||
export class TranslateCommand extends Command {
|
||||
public override registerApplicationCommands(
|
||||
registry: Command.Registry
|
||||
): void {
|
||||
registry.registerChatInputCommand(builder =>
|
||||
builder
|
||||
.setName(this.name)
|
||||
.setDescription(this.description)
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('target')
|
||||
.setDescription(
|
||||
'What is the target language?(language you want to translate to)'
|
||||
)
|
||||
.setRequired(true)
|
||||
)
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('text')
|
||||
.setDescription('What text do you want to translate?')
|
||||
.setRequired(true)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public override chatInputRun(
|
||||
interaction: Command.ChatInputCommandInteraction
|
||||
) {
|
||||
const targetLang = interaction.options.getString('target', true);
|
||||
|
||||
const text = interaction.options.getString('text', true);
|
||||
translate(text, {
|
||||
to: targetLang,
|
||||
requestFunction: axios
|
||||
})
|
||||
.then(async (response: any) => {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor('DarkRed')
|
||||
.setTitle('Google Translate')
|
||||
.setURL('https://translate.google.com/')
|
||||
.setDescription(response.text)
|
||||
.setFooter({
|
||||
iconURL: 'https://i.imgur.com/ZgFxIwe.png', // Google Translate Icon
|
||||
text: 'Powered by Google Translate'
|
||||
});
|
||||
|
||||
return await interaction.reply({ embeds: [embed] });
|
||||
})
|
||||
.catch(async error => {
|
||||
Logger.error(error);
|
||||
return await interaction.reply(
|
||||
':x: Something went wrong when trying to translate the text'
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
50
apps/bot/src/commands/other/trump.ts
Normal file
50
apps/bot/src/commands/other/trump.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import { Command, CommandOptions } from '@sapphire/framework';
|
||||
import { EmbedBuilder } from 'discord.js';
|
||||
import axios from 'axios';
|
||||
import Logger from '../../lib/logger';
|
||||
@ApplyOptions<CommandOptions>({
|
||||
name: 'trump',
|
||||
description: 'Replies with a random Trump quote',
|
||||
preconditions: ['isCommandDisabled']
|
||||
})
|
||||
export class TrumpCommand extends Command {
|
||||
public override registerApplicationCommands(
|
||||
registry: Command.Registry
|
||||
): void {
|
||||
registry.registerChatInputCommand({
|
||||
name: this.name,
|
||||
description: this.description
|
||||
});
|
||||
}
|
||||
|
||||
public override chatInputRun(
|
||||
interaction: Command.ChatInputCommandInteraction
|
||||
) {
|
||||
axios
|
||||
.get('https://api.tronalddump.io/random/quote')
|
||||
.then(async response => {
|
||||
const quote: string = response.data.value;
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor('Orange')
|
||||
.setAuthor({
|
||||
name: 'Donald Trump',
|
||||
url: 'https://api.tronalddump.io/random/quote',
|
||||
iconURL:
|
||||
'https://www.whitehouse.gov/wp-content/uploads/2021/01/45_donald_trump.jpg'
|
||||
})
|
||||
.setDescription(quote)
|
||||
.setTimestamp(response.data.appeared_at)
|
||||
.setFooter({
|
||||
text: 'Powered by api.tronalddump.io'
|
||||
});
|
||||
return interaction.reply({ embeds: [embed] });
|
||||
})
|
||||
.catch(async error => {
|
||||
Logger.error(error);
|
||||
return interaction.reply({
|
||||
content: 'Something went wrong when fetching a Trump quote :('
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
190
apps/bot/src/commands/other/tv-show-search.ts
Normal file
190
apps/bot/src/commands/other/tv-show-search.ts
Normal file
@@ -0,0 +1,190 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import { Command, CommandOptions } from '@sapphire/framework';
|
||||
import { PaginatedMessage } from '@sapphire/discord.js-utilities';
|
||||
import axios from 'axios';
|
||||
import Logger from '../../lib/logger';
|
||||
|
||||
@ApplyOptions<CommandOptions>({
|
||||
name: 'tv-show-search',
|
||||
description: 'Get TV shows information',
|
||||
preconditions: ['GuildOnly', 'isCommandDisabled']
|
||||
})
|
||||
export class TVShowSearchCommand extends Command {
|
||||
public override registerApplicationCommands(
|
||||
registry: Command.Registry
|
||||
): void {
|
||||
registry.registerChatInputCommand(builder =>
|
||||
builder
|
||||
.setName(this.name)
|
||||
.setDescription(this.description)
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('query')
|
||||
.setDescription('What TV show do you want to look up?')
|
||||
.setRequired(true)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public override async chatInputRun(
|
||||
interaction: Command.ChatInputCommandInteraction
|
||||
) {
|
||||
const query = interaction.options.getString('query', true);
|
||||
|
||||
try {
|
||||
var data = await this.getData(query);
|
||||
} catch (error: any) {
|
||||
return interaction.reply({ content: error });
|
||||
}
|
||||
|
||||
const PaginatedEmbed = new PaginatedMessage();
|
||||
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const showInfo = this.constructInfoObject(data[i].show);
|
||||
|
||||
PaginatedEmbed.addPageEmbed(embed =>
|
||||
embed
|
||||
.setTitle(showInfo.name)
|
||||
.setURL(showInfo.url)
|
||||
.setColor('DarkAqua')
|
||||
.setThumbnail(showInfo.thumbnail)
|
||||
.setDescription(showInfo.summary)
|
||||
.addFields(
|
||||
{ name: 'Language', value: showInfo.language, inline: true },
|
||||
{
|
||||
name: 'Genre(s)',
|
||||
value: showInfo.genres,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'Show Type',
|
||||
value: showInfo.type,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'Premiered',
|
||||
value: showInfo.premiered,
|
||||
inline: true
|
||||
},
|
||||
{ name: 'Network', value: showInfo.network, inline: true },
|
||||
|
||||
{ name: 'Runtime', value: showInfo.runtime, inline: true },
|
||||
{ name: 'Average Rating', value: showInfo.rating }
|
||||
)
|
||||
.setFooter({
|
||||
text: `(Page ${i}/${data.length}) Powered by tvmaze.com`,
|
||||
iconURL: 'https://static.tvmaze.com/images/favico/favicon-32x32.png'
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
await interaction.reply('Show info');
|
||||
return PaginatedEmbed.run(interaction);
|
||||
}
|
||||
|
||||
private getData(query: string): Promise<ResponseData> {
|
||||
return new Promise(async function (resolve, reject) {
|
||||
const url = `http://api.tvmaze.com/search/shows?q=${encodeURI(query)}`;
|
||||
try {
|
||||
const response = await axios.get(url);
|
||||
if (response.status == 429) {
|
||||
reject(':x: Rate Limit exceeded. Please try again in a few minutes.');
|
||||
}
|
||||
if (response.status == 503) {
|
||||
reject(
|
||||
':x: The service is currently unavailable. Please try again later.'
|
||||
);
|
||||
}
|
||||
if (response.status !== 200) {
|
||||
reject(
|
||||
'There was a problem getting data from the API, make sure you entered a valid TV show name'
|
||||
);
|
||||
}
|
||||
const data = response.data;
|
||||
if (!data.length) {
|
||||
reject(
|
||||
'There was a problem getting data from the API, make sure you entered a valid TV show name'
|
||||
);
|
||||
}
|
||||
resolve(data);
|
||||
} catch (e) {
|
||||
Logger.error(e);
|
||||
reject(
|
||||
'There was a problem getting data from the API, make sure you entered a valid TV show name'
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private constructInfoObject(show: any): InfoObject {
|
||||
return {
|
||||
name: show.name,
|
||||
url: show.url,
|
||||
summary: this.filterSummary(show.summary),
|
||||
language: this.checkIfNull(show.language),
|
||||
genres: this.checkGenres(show.genres),
|
||||
type: this.checkIfNull(show.type),
|
||||
premiered: this.checkIfNull(show.premiered),
|
||||
network: this.checkNetwork(show.network),
|
||||
runtime: show.runtime ? show.runtime + ' Minutes' : 'None Listed',
|
||||
rating: show.ratings ? show.rating.average : 'None Listed',
|
||||
thumbnail: show.image
|
||||
? show.image.original
|
||||
: 'https://static.tvmaze.com/images/no-img/no-img-portrait-text.png'
|
||||
};
|
||||
}
|
||||
|
||||
private filterSummary(summary: string) {
|
||||
return summary
|
||||
.replace(/<(\/)?b>/g, '**')
|
||||
.replace(/<(\/)?i>/g, '*')
|
||||
.replace(/<(\/)?p>/g, '')
|
||||
.replace(/<br>/g, '\n')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/'/g, "'")
|
||||
.replace(/"/g, '"')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
private checkGenres(genres: Genres) {
|
||||
if (Array.isArray(genres)) {
|
||||
if (genres.join(' ').trim().length == 0) return 'None Listed';
|
||||
return genres.join(' ');
|
||||
} else if (!genres.length) {
|
||||
return 'None Listed';
|
||||
}
|
||||
return genres;
|
||||
}
|
||||
|
||||
private checkIfNull(value: string) {
|
||||
if (!value) {
|
||||
return 'None Listed';
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private checkNetwork(network: any) {
|
||||
if (!network) return 'None Listed';
|
||||
return `(**${network.country.code}**) ${network.name}`;
|
||||
}
|
||||
}
|
||||
|
||||
type InfoObject = {
|
||||
name: string;
|
||||
url: string;
|
||||
summary: string;
|
||||
language: string;
|
||||
genres: string;
|
||||
type: string;
|
||||
premiered: string;
|
||||
network: string;
|
||||
runtime: string;
|
||||
rating: string;
|
||||
thumbnail: string;
|
||||
};
|
||||
|
||||
type Genres = string | Array<string>;
|
||||
|
||||
type ResponseData = string | Array<any>;
|
||||
59
apps/bot/src/commands/other/urban.ts
Normal file
59
apps/bot/src/commands/other/urban.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import { Command, CommandOptions } from '@sapphire/framework';
|
||||
import { EmbedBuilder } from 'discord.js';
|
||||
import axios from 'axios';
|
||||
import Logger from '../../lib/logger';
|
||||
|
||||
@ApplyOptions<CommandOptions>({
|
||||
name: 'urban',
|
||||
description: 'Get definitions from urban dictionary',
|
||||
preconditions: ['GuildOnly', 'isCommandDisabled']
|
||||
})
|
||||
export class UrbanCommand extends Command {
|
||||
public override registerApplicationCommands(
|
||||
registry: Command.Registry
|
||||
): void {
|
||||
registry.registerChatInputCommand(builder =>
|
||||
builder
|
||||
.setName(this.name)
|
||||
.setDescription(this.description)
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('query')
|
||||
.setDescription('What term do you want to look up?')
|
||||
.setRequired(true)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public override chatInputRun(
|
||||
interaction: Command.ChatInputCommandInteraction
|
||||
) {
|
||||
const query = interaction.options.getString('query', true);
|
||||
axios
|
||||
.get(`https://api.urbandictionary.com/v0/define?term=${query}`)
|
||||
.then(async response => {
|
||||
const definition: string = response.data.list[0].definition;
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor('DarkOrange')
|
||||
.setAuthor({
|
||||
name: 'Urban Dictionary',
|
||||
url: 'https://urbandictionary.com',
|
||||
iconURL: 'https://i.imgur.com/vdoosDm.png'
|
||||
})
|
||||
.setDescription(definition)
|
||||
.setURL(response.data.list[0].permalink)
|
||||
.setTimestamp()
|
||||
.setFooter({
|
||||
text: 'Powered by UrbanDictionary'
|
||||
});
|
||||
return interaction.reply({ embeds: [embed] });
|
||||
})
|
||||
.catch(async error => {
|
||||
Logger.error(error);
|
||||
return interaction.reply({
|
||||
content: 'Failed to deliver definition :sob:'
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
33
apps/bot/src/env.ts
Normal file
33
apps/bot/src/env.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { createEnv } from '@t3-oss/env-core';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const env = createEnv({
|
||||
/*
|
||||
* Specify what prefix the client-side variables must have.
|
||||
* This is enforced both on type-level and at runtime.
|
||||
*/
|
||||
clientPrefix: 'PUBLIC_',
|
||||
server: {
|
||||
DISCORD_TOKEN: z.string(),
|
||||
TENOR_API: z.string(),
|
||||
RAWG_API: z.string().optional(),
|
||||
// Redis
|
||||
REDIS_HOST: z.string().optional(),
|
||||
REDIS_PORT: z.string().optional(),
|
||||
REDIS_PASSWORD: z.string().optional(),
|
||||
REDIS_DB: z.string().optional(),
|
||||
// Lavalink
|
||||
LAVA_HOST: z.string().optional(),
|
||||
LAVA_PORT: z.string().optional(),
|
||||
LAVA_PASS: z.string().optional(),
|
||||
LAVA_SECURE: z.string().optional(),
|
||||
SPOTIFY_CLIENT_ID: z.string().optional(),
|
||||
SPOTIFY_CLIENT_SECRET: z.string().optional()
|
||||
},
|
||||
client: {},
|
||||
/**
|
||||
* What object holds the environment variables at runtime.
|
||||
* Often `process.env` or `import.meta.env`
|
||||
*/
|
||||
runtimeEnv: process.env
|
||||
});
|
||||
76
apps/bot/src/index.ts
Normal file
76
apps/bot/src/index.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { ExtendedClient } from './lib/structures/ExtendedClient';
|
||||
import { env } from './env';
|
||||
import { load } from '@lavaclient/spotify';
|
||||
import {
|
||||
ApplicationCommandRegistries,
|
||||
RegisterBehavior
|
||||
} from '@sapphire/framework';
|
||||
import { ActivityType } from 'discord.js';
|
||||
|
||||
ApplicationCommandRegistries.setDefaultBehaviorWhenNotIdentical(
|
||||
RegisterBehavior.Overwrite
|
||||
);
|
||||
|
||||
if (env.SPOTIFY_CLIENT_ID && env.SPOTIFY_CLIENT_SECRET) {
|
||||
load({
|
||||
client: {
|
||||
id: env.SPOTIFY_CLIENT_ID,
|
||||
secret: env.SPOTIFY_CLIENT_SECRET
|
||||
},
|
||||
autoResolveYoutubeTracks: true
|
||||
});
|
||||
}
|
||||
|
||||
const client = new ExtendedClient();
|
||||
|
||||
client.on('ready', () => {
|
||||
client.music.connect(client.user!.id);
|
||||
client.user?.setActivity('/', {
|
||||
type: ActivityType.Watching
|
||||
});
|
||||
|
||||
client.user?.setStatus('online');
|
||||
});
|
||||
|
||||
client.on('chatInputCommandError', err => {
|
||||
console.log('Command Chat Input ' + err);
|
||||
});
|
||||
client.on('contextMenuCommandError', err => {
|
||||
console.log('Command Context Menu ' + err);
|
||||
});
|
||||
client.on('commandAutocompleteInteractionError', err => {
|
||||
console.log('Command Autocomplete ' + err);
|
||||
});
|
||||
client.on('commandApplicationCommandRegistryError', err => {
|
||||
console.log('Command Registry ' + err);
|
||||
});
|
||||
client.on('messageCommandError', err => {
|
||||
console.log('Command ' + err);
|
||||
});
|
||||
client.on('interactionHandlerError', err => {
|
||||
console.log('Interaction ' + err);
|
||||
});
|
||||
client.on('interactionHandlerParseError', err => {
|
||||
console.log('Interaction Parse ' + err);
|
||||
});
|
||||
|
||||
client.on('listenerError', err => {
|
||||
console.log('Client Listener ' + err);
|
||||
});
|
||||
|
||||
// LavaLink
|
||||
client.music.on('error', err => {
|
||||
console.log('LavaLink ' + err);
|
||||
});
|
||||
|
||||
const main = async () => {
|
||||
try {
|
||||
await client.login(env.DISCORD_TOKEN);
|
||||
} catch (error) {
|
||||
console.log('Bot errored out', error);
|
||||
client.destroy();
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
void main();
|
||||
4
apps/bot/src/lib/constants.ts
Normal file
4
apps/bot/src/lib/constants.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { join } from 'path';
|
||||
|
||||
export const rootDir = join(__dirname, '..', '..');
|
||||
export const srcDir = join(rootDir, 'src');
|
||||
54
apps/bot/src/lib/logger.ts
Normal file
54
apps/bot/src/lib/logger.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import winston from 'winston';
|
||||
import 'winston-daily-rotate-file';
|
||||
|
||||
const levels = {
|
||||
error: 0,
|
||||
warn: 1,
|
||||
info: 2,
|
||||
http: 3,
|
||||
debug: 4
|
||||
};
|
||||
|
||||
const level = () => {
|
||||
const env = process.env.NODE_ENV || 'development';
|
||||
const isDevelopment = env === 'development';
|
||||
return isDevelopment ? 'debug' : 'warn';
|
||||
};
|
||||
|
||||
const colors = {
|
||||
error: 'red',
|
||||
warn: 'yellow',
|
||||
info: 'green',
|
||||
http: 'magenta',
|
||||
debug: 'white'
|
||||
};
|
||||
|
||||
winston.addColors(colors);
|
||||
|
||||
const format = winston.format.combine(
|
||||
winston.format.timestamp({ format: 'MM-DD-YYYY HH:mm:ss' }),
|
||||
// winston.format.colorize({ level: true }),
|
||||
winston.format.printf(
|
||||
info => `${info.timestamp} ${info.level}: ${info.message}`
|
||||
)
|
||||
);
|
||||
const transports = [
|
||||
new winston.transports.Console(),
|
||||
new winston.transports.DailyRotateFile({
|
||||
dirname: './logs',
|
||||
filename: 'Master-Bot-%DATE%.log',
|
||||
datePattern: 'MM-DD-YYYY',
|
||||
zippedArchive: true,
|
||||
maxSize: '20m',
|
||||
maxFiles: '14d'
|
||||
})
|
||||
];
|
||||
|
||||
const Logger = winston.createLogger({
|
||||
level: level(),
|
||||
levels,
|
||||
format,
|
||||
transports
|
||||
});
|
||||
|
||||
export default Logger;
|
||||
64
apps/bot/src/lib/music/buttonHandler.ts
Normal file
64
apps/bot/src/lib/music/buttonHandler.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import type { Song } from './classes/Song';
|
||||
import { container } from '@sapphire/framework';
|
||||
import type { Queue } from './classes/Queue';
|
||||
import {
|
||||
Message,
|
||||
ActionRowBuilder,
|
||||
ButtonBuilder,
|
||||
EmbedBuilder,
|
||||
ButtonStyle
|
||||
} from 'discord.js';
|
||||
import buttonsCollector, { deletePlayerEmbed } from './buttonsCollector';
|
||||
|
||||
export async function embedButtons(
|
||||
embed: EmbedBuilder,
|
||||
queue: Queue,
|
||||
song: Song,
|
||||
message?: string
|
||||
) {
|
||||
await deletePlayerEmbed(queue);
|
||||
|
||||
const { client } = container;
|
||||
const tracks = await queue.tracks();
|
||||
const row = new ActionRowBuilder<ButtonBuilder>().addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId('playPause')
|
||||
.setLabel('Play/Pause')
|
||||
.setStyle(ButtonStyle.Primary),
|
||||
new ButtonBuilder()
|
||||
.setCustomId('stop')
|
||||
.setLabel('Stop')
|
||||
.setStyle(ButtonStyle.Danger),
|
||||
new ButtonBuilder()
|
||||
.setCustomId('next')
|
||||
.setLabel('Next')
|
||||
.setStyle(ButtonStyle.Primary)
|
||||
.setDisabled(!tracks.length ? true : false),
|
||||
new ButtonBuilder()
|
||||
.setCustomId('volumeUp')
|
||||
.setLabel('Vol+')
|
||||
.setStyle(ButtonStyle.Primary),
|
||||
new ButtonBuilder()
|
||||
.setCustomId('volumeDown')
|
||||
.setLabel('Vol-')
|
||||
.setStyle(ButtonStyle.Primary)
|
||||
);
|
||||
|
||||
const channel = await queue.getTextChannel();
|
||||
if (!channel) return;
|
||||
|
||||
return await channel
|
||||
.send({
|
||||
embeds: [embed],
|
||||
components: [row],
|
||||
content: message
|
||||
})
|
||||
.then(async (message: Message) => {
|
||||
const queue = client.music.queues.get(message.guild!.id);
|
||||
await queue.setEmbed(message.id);
|
||||
|
||||
if (queue.player) {
|
||||
await buttonsCollector(message, song);
|
||||
}
|
||||
});
|
||||
}
|
||||
131
apps/bot/src/lib/music/buttonsCollector.ts
Normal file
131
apps/bot/src/lib/music/buttonsCollector.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import { Time } from '@sapphire/time-utilities';
|
||||
import type { Message, MessageComponentInteraction } from 'discord.js';
|
||||
import { container } from '@sapphire/framework';
|
||||
import type { Queue } from './classes/Queue';
|
||||
import { NowPlayingEmbed } from './nowPlayingEmbed';
|
||||
import type { Song } from './classes/Song';
|
||||
import Logger from '../logger';
|
||||
|
||||
export default async function buttonsCollector(message: Message, song: Song) {
|
||||
const { client } = container;
|
||||
|
||||
const queue = client.music.queues.get(message.guildId!);
|
||||
const channel = await queue.getTextChannel();
|
||||
|
||||
const collector = message.createMessageComponentCollector();
|
||||
if (!channel) return;
|
||||
|
||||
const maxLimit = Time.Minute * 30;
|
||||
let timer: NodeJS.Timer;
|
||||
|
||||
collector.on('collect', async (i: MessageComponentInteraction) => {
|
||||
if (!message.member?.voice.channel?.members.has(i.user.id)) {
|
||||
await i.reply({
|
||||
content: `:x: Only available to members in ${message.member?.voice.channel} <-- Click To Join`,
|
||||
ephemeral: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (i.customId === 'playPause') {
|
||||
if (queue.paused) {
|
||||
await queue.resume();
|
||||
clearTimeout(client.leaveTimers[queue.guildID]!);
|
||||
} else {
|
||||
client.leaveTimers[queue.guildID] = setTimeout(async () => {
|
||||
await channel.send(':zzz: Leaving due to inactivity');
|
||||
await queue.leave();
|
||||
}, maxLimit);
|
||||
await queue.pause();
|
||||
}
|
||||
|
||||
const tracks = await queue.tracks();
|
||||
const NowPlaying = new NowPlayingEmbed(
|
||||
song,
|
||||
queue.player.accuratePosition,
|
||||
queue.player.trackData?.length ?? 0,
|
||||
queue.player.volume,
|
||||
tracks,
|
||||
tracks.at(-1),
|
||||
queue.player.paused
|
||||
);
|
||||
collector.empty();
|
||||
await i.update({
|
||||
embeds: [await NowPlaying.NowPlayingEmbed()]
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (i.customId === 'stop') {
|
||||
clearTimeout(timer);
|
||||
await queue.leave();
|
||||
return;
|
||||
}
|
||||
if (i.customId === 'next') {
|
||||
clearTimeout(timer);
|
||||
await queue.next({ skipped: true });
|
||||
return;
|
||||
}
|
||||
if (i.customId === 'volumeUp') {
|
||||
const currentVolume = await queue.getVolume();
|
||||
const volume = currentVolume + 10 > 200 ? 200 : currentVolume + 10;
|
||||
await queue.setVolume(volume);
|
||||
const tracks = await queue.tracks();
|
||||
const NowPlaying = new NowPlayingEmbed(
|
||||
song,
|
||||
queue.player.accuratePosition,
|
||||
queue.player.trackData?.length ?? 0,
|
||||
queue.player.volume,
|
||||
tracks,
|
||||
tracks.at(-1),
|
||||
queue.player.paused
|
||||
);
|
||||
collector.empty();
|
||||
await i.update({
|
||||
embeds: [await NowPlaying.NowPlayingEmbed()]
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (i.customId === 'volumeDown') {
|
||||
const currentVolume = await queue.getVolume();
|
||||
const volume = currentVolume - 10 < 0 ? 0 : currentVolume - 10;
|
||||
await queue.setVolume(volume);
|
||||
const tracks = await queue.tracks();
|
||||
const NowPlaying = new NowPlayingEmbed(
|
||||
song,
|
||||
queue.player.accuratePosition,
|
||||
queue.player.trackData?.length ?? 0,
|
||||
queue.player.volume,
|
||||
tracks,
|
||||
tracks.at(-1),
|
||||
queue.player.paused
|
||||
);
|
||||
collector.empty();
|
||||
await i.update({ embeds: [await NowPlaying.NowPlayingEmbed()] });
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
collector.on('end', async () => {
|
||||
clearTimeout(timer);
|
||||
});
|
||||
|
||||
return collector;
|
||||
}
|
||||
|
||||
export async function deletePlayerEmbed(queue: Queue) {
|
||||
try {
|
||||
const embedID = await queue.getEmbed();
|
||||
if (embedID) {
|
||||
const channel = await queue.getTextChannel();
|
||||
await channel?.messages.fetch(embedID).then(async oldMessage => {
|
||||
if (oldMessage)
|
||||
await oldMessage.delete().catch(error => {
|
||||
Logger.error('Failed to Delete Old Message. ' + error);
|
||||
});
|
||||
await queue.deleteEmbed();
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error('Failed to Delete Player Embed. ' + error);
|
||||
}
|
||||
}
|
||||
58
apps/bot/src/lib/music/channelHandler.ts
Normal file
58
apps/bot/src/lib/music/channelHandler.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import type { Queue } from './classes/Queue';
|
||||
import { Channel, GuildMember, ChannelType } from 'discord.js';
|
||||
import Logger from '../logger';
|
||||
|
||||
export async function manageStageChannel(
|
||||
voiceChannel: Channel,
|
||||
botUser: GuildMember,
|
||||
instance: Queue
|
||||
) {
|
||||
if (voiceChannel.type !== ChannelType.GuildStageVoice) return;
|
||||
// Stage Channel Permissions From Discord.js Doc's
|
||||
if (
|
||||
!botUser?.permissions.has(
|
||||
('ManageChannels' && 'MuteMembers' && 'MoveMembers') || 'ADMINISTRATOR'
|
||||
)
|
||||
)
|
||||
if (botUser.voice.suppress)
|
||||
return await instance.getTextChannel().then(
|
||||
async msg =>
|
||||
await msg?.send({
|
||||
content: `:interrobang: Please make promote me to a Speaker in ${voiceChannel.name}, Missing permissions "Administrator" ***OR*** "Manage Channels, Mute Members, and Move Members" for Full Stage Channel Features.`
|
||||
})
|
||||
);
|
||||
const tracks = await instance.tracks();
|
||||
const title =
|
||||
instance.player.trackData?.title.length! > 114
|
||||
? `🎶 ${
|
||||
instance.player.trackData?.title.slice(0, 114) ??
|
||||
tracks.at(0)?.title.slice(0, 114)
|
||||
}...`
|
||||
: `🎶 ${instance.player.trackData?.title ?? tracks.at(0)?.title ?? ''}`;
|
||||
|
||||
if (!voiceChannel.stageInstance) {
|
||||
await voiceChannel
|
||||
.createStageInstance({
|
||||
topic: title,
|
||||
privacyLevel: 2 // Guild Only
|
||||
})
|
||||
.catch(error => {
|
||||
Logger.error('Failed to Create a Stage Instance. ' + error);
|
||||
});
|
||||
}
|
||||
|
||||
if (botUser?.voice.suppress)
|
||||
await botUser?.voice.setSuppressed(false).catch((error: string) => {
|
||||
Logger.error('Failed to Set Suppressed to False. ' + error);
|
||||
});
|
||||
|
||||
if (
|
||||
voiceChannel.stageInstance?.topic.startsWith('🎶') &&
|
||||
voiceChannel.stageInstance?.topic !== title
|
||||
) {
|
||||
await voiceChannel.stageInstance?.setTopic(title).catch(error => {
|
||||
Logger.error('Failed to Set Topic. ' + error);
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
471
apps/bot/src/lib/music/classes/Queue.ts
Normal file
471
apps/bot/src/lib/music/classes/Queue.ts
Normal file
@@ -0,0 +1,471 @@
|
||||
// Inspired from skyra's queue(when it had a music feature)
|
||||
import type {
|
||||
CommandInteraction,
|
||||
Guild,
|
||||
GuildMember,
|
||||
TextChannel,
|
||||
VoiceChannel
|
||||
} from 'discord.js';
|
||||
import type { Song } from './Song';
|
||||
import type { Track } from '@lavaclient/types/v3';
|
||||
import type { DiscordResource, Player, Snowflake } from 'lavaclient';
|
||||
import { container } from '@sapphire/framework';
|
||||
import type { QueueStore } from './QueueStore';
|
||||
import { Time } from '@sapphire/time-utilities';
|
||||
import { isNullish } from '@sapphire/utilities';
|
||||
import { deletePlayerEmbed } from '../buttonsCollector';
|
||||
import { trpcNode } from '../../../trpc';
|
||||
import Logger from '../../logger';
|
||||
|
||||
export enum LoopType {
|
||||
None,
|
||||
Queue,
|
||||
Song
|
||||
}
|
||||
|
||||
const kExpireTime = Time.Day * 2;
|
||||
|
||||
export interface QueueEvents {
|
||||
trackStart: (song: Song) => void;
|
||||
trackEnd: (song: Song) => void;
|
||||
finish: () => void;
|
||||
}
|
||||
|
||||
export interface Loop {
|
||||
type: LoopType;
|
||||
current: number;
|
||||
max: number;
|
||||
}
|
||||
|
||||
export interface AddOptions {
|
||||
requester?: Snowflake | DiscordResource;
|
||||
userInfo?: GuildMember;
|
||||
added?: number;
|
||||
next?: boolean;
|
||||
}
|
||||
|
||||
export type Addable = string | Track | Song;
|
||||
|
||||
interface NowPlaying {
|
||||
song: Song;
|
||||
position: number;
|
||||
}
|
||||
|
||||
interface QueueKeys {
|
||||
readonly next: string;
|
||||
readonly position: string;
|
||||
readonly current: string;
|
||||
readonly skips: string;
|
||||
readonly systemPause: string;
|
||||
readonly replay: string;
|
||||
readonly volume: string;
|
||||
readonly text: string;
|
||||
readonly embed: string;
|
||||
}
|
||||
|
||||
export class Queue {
|
||||
public readonly keys: QueueKeys;
|
||||
private skipped: boolean;
|
||||
|
||||
public constructor(
|
||||
public readonly store: QueueStore,
|
||||
public readonly guildID: string
|
||||
) {
|
||||
this.keys = {
|
||||
current: `music.${this.guildID}.current`,
|
||||
next: `music.${this.guildID}.next`,
|
||||
position: `music.${this.guildID}.position`,
|
||||
skips: `music.${this.guildID}.skips`,
|
||||
systemPause: `music.${this.guildID}.systemPause`,
|
||||
replay: `music.${this.guildID}.replay`,
|
||||
volume: `music.${this.guildID}.volume`,
|
||||
text: `music.${this.guildID}.text`,
|
||||
embed: `music.${this.guildID}.embed`
|
||||
};
|
||||
|
||||
this.skipped = false;
|
||||
}
|
||||
|
||||
public get client() {
|
||||
return container.client;
|
||||
}
|
||||
|
||||
public get player(): Player {
|
||||
return this.store.client.players.get(this.guildID)!;
|
||||
}
|
||||
|
||||
public get playing(): boolean {
|
||||
return this.player.playing;
|
||||
}
|
||||
|
||||
public get paused(): boolean {
|
||||
return this.player.paused;
|
||||
}
|
||||
|
||||
public get guild(): Guild {
|
||||
return this.client.guilds.cache.get(this.guildID) as Guild;
|
||||
}
|
||||
|
||||
public get voiceChannel(): VoiceChannel | null {
|
||||
const id = this.voiceChannelID;
|
||||
return id
|
||||
? (this.guild.channels.cache.get(id) as VoiceChannel) ?? null
|
||||
: null;
|
||||
}
|
||||
|
||||
public get voiceChannelID(): string | null {
|
||||
if (!this.player) return null;
|
||||
return this.player.channelId ?? null;
|
||||
}
|
||||
|
||||
public createPlayer(): Player {
|
||||
let player = this.player;
|
||||
if (!player) {
|
||||
player = this.store.client.createPlayer(this.guildID);
|
||||
player.on('trackEnd', async () => {
|
||||
if (!this.skipped) {
|
||||
await this.next();
|
||||
}
|
||||
this.skipped = false;
|
||||
});
|
||||
}
|
||||
return player;
|
||||
}
|
||||
|
||||
public destroyPlayer(): void {
|
||||
if (this.player) {
|
||||
this.store.client.destroyPlayer(this.guildID);
|
||||
}
|
||||
}
|
||||
|
||||
// Start the queue
|
||||
public async start(replaying = false): Promise<boolean> {
|
||||
const np = await this.nowPlaying();
|
||||
if (!np) return this.next();
|
||||
|
||||
try {
|
||||
this.player.setVolume(await this.getVolume());
|
||||
await this.player.play(np.song as Song);
|
||||
} catch (err) {
|
||||
Logger.error(err);
|
||||
await this.leave();
|
||||
}
|
||||
|
||||
this.client.emit(
|
||||
replaying ? 'musicSongReplay' : 'musicSongPlay',
|
||||
this,
|
||||
np.song as Song
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Returns whether or not there are songs that can be played
|
||||
public async canStart(): Promise<boolean> {
|
||||
return (
|
||||
(await this.store.redis.exists(this.keys.current, this.keys.next)) > 0
|
||||
);
|
||||
}
|
||||
|
||||
// add tracks to queue
|
||||
public async add(
|
||||
songs: Song | Array<Song>,
|
||||
options: AddOptions = {}
|
||||
): Promise<number> {
|
||||
songs = Array.isArray(songs) ? songs : [songs];
|
||||
if (!songs.length) return 0;
|
||||
|
||||
await this.store.redis.lpush(
|
||||
this.keys.next,
|
||||
...songs.map(song => this.stringifySong(song))
|
||||
);
|
||||
await this.refresh();
|
||||
return songs.length;
|
||||
}
|
||||
|
||||
public async pause(interaction?: CommandInteraction) {
|
||||
await this.player.pause(true);
|
||||
await this.setSystemPaused(false);
|
||||
if (interaction) {
|
||||
this.client.emit('musicSongPause', interaction);
|
||||
}
|
||||
}
|
||||
|
||||
public async resume(interaction?: CommandInteraction) {
|
||||
await this.player.pause(false);
|
||||
await this.setSystemPaused(false);
|
||||
if (interaction) {
|
||||
this.client.emit('musicSongResume', interaction);
|
||||
}
|
||||
}
|
||||
|
||||
public async getSystemPaused(): Promise<boolean> {
|
||||
return await this.store.redis
|
||||
.get(this.keys.systemPause)
|
||||
.then(d => d === '1');
|
||||
}
|
||||
|
||||
public async setSystemPaused(value: boolean): Promise<boolean> {
|
||||
await this.store.redis.set(this.keys.systemPause, value ? '1' : '0');
|
||||
await this.refresh();
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves whether or not the system should repeat the current track.
|
||||
*/
|
||||
public async getReplay(): Promise<boolean> {
|
||||
return await this.store.redis.get(this.keys.replay).then(d => d === '1');
|
||||
}
|
||||
|
||||
public async setReplay(value: boolean): Promise<boolean> {
|
||||
await this.store.redis.set(this.keys.replay, value ? '1' : '0');
|
||||
await this.refresh();
|
||||
this.client.emit('musicReplayUpdate', this, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the volume of the track in the queue.
|
||||
*/
|
||||
|
||||
public async getVolume(): Promise<number> {
|
||||
let data = await this.store.redis.get(this.keys.volume);
|
||||
|
||||
if (!data) {
|
||||
const guildQuery = await trpcNode.guild.getGuild.query({
|
||||
id: this.guildID
|
||||
});
|
||||
|
||||
if (!guildQuery || !guildQuery.guild)
|
||||
await this.setVolume(this.player.volume ?? 100); // saves to both
|
||||
|
||||
if (guildQuery.guild)
|
||||
data =
|
||||
guildQuery.guild.volume.toString() || this.player.volume.toString();
|
||||
}
|
||||
|
||||
return data ? Number(data) : 100;
|
||||
}
|
||||
|
||||
// set the volume of the track in the queue
|
||||
public async setVolume(
|
||||
value: number
|
||||
): Promise<{ previous: number; next: number }> {
|
||||
await this.player.setVolume(value);
|
||||
const previous = await this.store.redis.getset(this.keys.volume, value);
|
||||
await this.refresh();
|
||||
|
||||
await trpcNode.guild.updateVolume.mutate({
|
||||
guildId: this.guildID,
|
||||
volume: this.player.volume
|
||||
});
|
||||
|
||||
this.client.emit('musicSongVolumeUpdate', this, value);
|
||||
return {
|
||||
previous: previous === null ? 100 : Number(previous),
|
||||
next: value
|
||||
};
|
||||
}
|
||||
|
||||
public async seek(position: number): Promise<void> {
|
||||
await this.player.seek(position);
|
||||
}
|
||||
|
||||
// connect to a voice channel
|
||||
public async connect(channelID: string): Promise<void> {
|
||||
await this.player.connect(channelID, { deafened: true });
|
||||
}
|
||||
|
||||
// leave the voice channel
|
||||
public async leave(): Promise<void> {
|
||||
if (await this.getEmbed()) {
|
||||
await deletePlayerEmbed(this);
|
||||
}
|
||||
if (this.client.leaveTimers[this.guildID]) {
|
||||
clearTimeout(this.client.leaveTimers[this.player.guildId]);
|
||||
delete this.client.leaveTimers[this.player.guildId];
|
||||
}
|
||||
if (!this.player) return;
|
||||
await this.player.disconnect();
|
||||
await this.destroyPlayer();
|
||||
await this.setTextChannelID(null);
|
||||
await this.clear();
|
||||
}
|
||||
|
||||
public async getTextChannel(): Promise<TextChannel | null> {
|
||||
const id = await this.getTextChannelID();
|
||||
if (id === null) return null;
|
||||
|
||||
const channel = this.guild.channels.cache.get(id) ?? null;
|
||||
if (channel === null) {
|
||||
await this.setTextChannelID(null);
|
||||
return null;
|
||||
}
|
||||
|
||||
return channel as TextChannel;
|
||||
}
|
||||
|
||||
public getTextChannelID(): Promise<string | null> {
|
||||
return this.store.redis.get(this.keys.text);
|
||||
}
|
||||
|
||||
public setTextChannelID(channelID: null): Promise<null>;
|
||||
|
||||
public async setTextChannelID(channelID: string): Promise<string>;
|
||||
public async setTextChannelID(
|
||||
channelID: string | null
|
||||
): Promise<string | null> {
|
||||
if (channelID === null) {
|
||||
await this.store.redis.del(this.keys.text);
|
||||
} else {
|
||||
await this.store.redis.set(this.keys.text, channelID);
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
return channelID;
|
||||
}
|
||||
|
||||
public async getCurrentTrack(): Promise<Song | null> {
|
||||
const value = await this.store.redis.get(this.keys.current);
|
||||
return value ? this.parseSongString(value) : null;
|
||||
}
|
||||
|
||||
public async getAt(index: number): Promise<Song | undefined> {
|
||||
const value = await this.store.redis.lindex(this.keys.next, -index - 1);
|
||||
return value ? this.parseSongString(value) : undefined;
|
||||
}
|
||||
|
||||
public async removeAt(position: number): Promise<void> {
|
||||
await this.store.redis.lremat(this.keys.next, -position - 1);
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
public async next({ skipped = false } = {}): Promise<boolean> {
|
||||
if (skipped) this.skipped = true;
|
||||
// Sets the current position to 0.
|
||||
await this.store.redis.del(this.keys.position);
|
||||
|
||||
// Get whether or not the queue is on replay mode.
|
||||
const replaying = await this.getReplay();
|
||||
|
||||
// If not skipped (song ended) and is replaying, replay.
|
||||
if (!skipped && replaying) {
|
||||
return await this.start(true);
|
||||
}
|
||||
|
||||
// If it was skipped, set replay back to false.
|
||||
if (replaying) await this.setReplay(false);
|
||||
|
||||
// Removes the next entry from the list and sets it as the current track.
|
||||
const entry = await this.store.redis.rpopset(
|
||||
this.keys.next,
|
||||
this.keys.current
|
||||
);
|
||||
// If there was an entry to play, refresh the state and start playing.
|
||||
if (entry) {
|
||||
await this.refresh();
|
||||
return this.start(false);
|
||||
} else {
|
||||
// If there was no entry, disconnect from the voice channel.
|
||||
await this.leave();
|
||||
this.client.emit('musicFinish', this, true);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public count(): Promise<number> {
|
||||
return this.store.redis.llen(this.keys.next);
|
||||
}
|
||||
|
||||
public async moveTracks(from: number, to: number): Promise<void> {
|
||||
await this.store.redis.lmove(this.keys.next, -from - 1, -to - 1); // work from the end of the list, since it's reversed
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
public async shuffleTracks(): Promise<void> {
|
||||
await this.store.redis.lshuffle(this.keys.next, Date.now());
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
public async stop(): Promise<void> {
|
||||
await this.player.stop();
|
||||
}
|
||||
|
||||
public async clearTracks(): Promise<void> {
|
||||
await this.store.redis.del(this.keys.next);
|
||||
}
|
||||
|
||||
public async skipTo(position: number): Promise<void> {
|
||||
await this.store.redis.ltrim(this.keys.next, 0, position - 1);
|
||||
await this.next({ skipped: true });
|
||||
}
|
||||
|
||||
public refresh() {
|
||||
return this.store.redis
|
||||
.pipeline()
|
||||
.pexpire(this.keys.next, kExpireTime)
|
||||
.pexpire(this.keys.position, kExpireTime)
|
||||
.pexpire(this.keys.current, kExpireTime)
|
||||
.pexpire(this.keys.skips, kExpireTime)
|
||||
.pexpire(this.keys.systemPause, kExpireTime)
|
||||
.pexpire(this.keys.replay, kExpireTime)
|
||||
.pexpire(this.keys.volume, kExpireTime)
|
||||
.pexpire(this.keys.text, kExpireTime)
|
||||
.pexpire(this.keys.embed, kExpireTime)
|
||||
.exec();
|
||||
}
|
||||
|
||||
public clear(): Promise<number> {
|
||||
return this.store.redis.del(
|
||||
this.keys.next,
|
||||
this.keys.position,
|
||||
this.keys.current,
|
||||
this.keys.skips,
|
||||
this.keys.systemPause,
|
||||
this.keys.replay,
|
||||
this.keys.volume,
|
||||
this.keys.text,
|
||||
this.keys.embed
|
||||
);
|
||||
}
|
||||
|
||||
public async nowPlaying(): Promise<NowPlaying | null> {
|
||||
const [entry, position] = await Promise.all([
|
||||
this.getCurrentTrack(),
|
||||
this.store.redis.get(this.keys.position)
|
||||
]);
|
||||
if (entry === null) return null;
|
||||
|
||||
return {
|
||||
song: entry,
|
||||
position: isNullish(position) ? 0 : parseInt(position, 10)
|
||||
};
|
||||
}
|
||||
|
||||
public async tracks(start = 0, end = -1): Promise<Song[]> {
|
||||
if (end === Infinity) end = -1;
|
||||
|
||||
const tracks = await this.store.redis.lrange(this.keys.next, start, end);
|
||||
return [...tracks].map(this.parseSongString).reverse();
|
||||
}
|
||||
|
||||
public async setEmbed(id: string): Promise<void> {
|
||||
await this.store.redis.set(this.keys.embed, id);
|
||||
}
|
||||
|
||||
public async getEmbed(): Promise<string | null> {
|
||||
return this.store.redis.get(this.keys.embed);
|
||||
}
|
||||
|
||||
public async deleteEmbed(): Promise<void> {
|
||||
await this.store.redis.del(this.keys.embed);
|
||||
}
|
||||
|
||||
public stringifySong(song: Song): string {
|
||||
return JSON.stringify(song);
|
||||
}
|
||||
|
||||
public parseSongString(song: string): Song {
|
||||
return JSON.parse(song);
|
||||
}
|
||||
}
|
||||
30
apps/bot/src/lib/music/classes/QueueClient.ts
Normal file
30
apps/bot/src/lib/music/classes/QueueClient.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import Redis from 'ioredis';
|
||||
import type { RedisOptions } from 'ioredis';
|
||||
import { ConnectionInfo, Node, SendGatewayPayload } from 'lavaclient';
|
||||
import { QueueStore } from './QueueStore';
|
||||
|
||||
export interface QueueClientOptions {
|
||||
redis: Redis | RedisOptions;
|
||||
}
|
||||
|
||||
export interface ConstructorTypes {
|
||||
options: QueueClientOptions;
|
||||
sendGatewayPayload: SendGatewayPayload;
|
||||
connection: ConnectionInfo;
|
||||
}
|
||||
|
||||
export class QueueClient extends Node {
|
||||
public readonly queues: QueueStore;
|
||||
|
||||
public constructor({
|
||||
options,
|
||||
sendGatewayPayload,
|
||||
connection
|
||||
}: ConstructorTypes) {
|
||||
super({ ...options, sendGatewayPayload, connection });
|
||||
this.queues = new QueueStore(
|
||||
this,
|
||||
options.redis instanceof Redis ? options.redis : new Redis(options.redis)
|
||||
);
|
||||
}
|
||||
}
|
||||
107
apps/bot/src/lib/music/classes/QueueStore.ts
Normal file
107
apps/bot/src/lib/music/classes/QueueStore.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { Collection } from 'discord.js';
|
||||
import { readFileSync } from 'fs';
|
||||
import type { Redis, RedisKey } from 'ioredis';
|
||||
import { join, resolve } from 'path';
|
||||
import { Queue } from './Queue';
|
||||
import type { QueueClient } from './QueueClient';
|
||||
import Logger from '../../logger';
|
||||
|
||||
interface RedisCommand {
|
||||
name: string;
|
||||
keys: number;
|
||||
}
|
||||
|
||||
const commands: RedisCommand[] = [
|
||||
{
|
||||
name: 'lmove',
|
||||
keys: 1
|
||||
},
|
||||
{
|
||||
name: 'lremat',
|
||||
keys: 1
|
||||
},
|
||||
{
|
||||
name: 'lshuffle',
|
||||
keys: 1
|
||||
},
|
||||
{
|
||||
name: 'rpopset',
|
||||
keys: 2
|
||||
}
|
||||
];
|
||||
|
||||
//@ts-ignore
|
||||
export interface ExtendedRedis extends Redis {
|
||||
lmove: (key: RedisKey, from: number, to: number) => Promise<'OK'>;
|
||||
lremat: (key: RedisKey, index: number) => Promise<'OK'>;
|
||||
lshuffle: (key: RedisKey, seed: number) => Promise<'OK'>;
|
||||
rpopset: (source: RedisKey, destination: RedisKey) => Promise<string | null>;
|
||||
}
|
||||
|
||||
export class QueueStore extends Collection<string, Queue> {
|
||||
public redis: ExtendedRedis;
|
||||
|
||||
public constructor(
|
||||
public readonly client: QueueClient,
|
||||
redis: Redis
|
||||
) {
|
||||
super();
|
||||
this.redis = redis as any;
|
||||
// Redis Errors
|
||||
redis.on('error', err => {
|
||||
Logger.error('Redis ' + err);
|
||||
});
|
||||
|
||||
for (const command of commands) {
|
||||
this.redis.defineCommand(command.name, {
|
||||
numberOfKeys: command.keys,
|
||||
lua: readFileSync(
|
||||
resolve(
|
||||
join(__dirname, '..', '..', '..'),
|
||||
'audio',
|
||||
`${command.name}.lua`
|
||||
)
|
||||
).toString()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public get(key: string): Queue {
|
||||
let queue = super.get(key);
|
||||
if (!queue) {
|
||||
queue = new Queue(this, key);
|
||||
this.set(key, queue);
|
||||
}
|
||||
return queue;
|
||||
}
|
||||
|
||||
public async start() {
|
||||
const guilds = await this.getPlayingEntries();
|
||||
await Promise.all(guilds.map(guild => this.get(guild).start()));
|
||||
}
|
||||
|
||||
private async getPlayingEntries(): Promise<string[]> {
|
||||
const guilds = new Set<string>();
|
||||
|
||||
let cursor = '0';
|
||||
do {
|
||||
// `scan` returns a tuple with the next cursor (which must be used for the
|
||||
// next iteration) and an array of the matching keys. The iterations end when
|
||||
// cursor becomes '0' again.
|
||||
const response = await this.redis.scan(
|
||||
cursor,
|
||||
'MATCH',
|
||||
'music.*.position'
|
||||
);
|
||||
[cursor] = response;
|
||||
|
||||
for (const key of response[1]) {
|
||||
// Slice 'skyra.a.' from the start, and '.p' from the end:
|
||||
const id = key.slice(8, -2);
|
||||
guilds.add(id);
|
||||
}
|
||||
} while (cursor !== '0');
|
||||
|
||||
return [...guilds];
|
||||
}
|
||||
}
|
||||
94
apps/bot/src/lib/music/classes/Song.ts
Normal file
94
apps/bot/src/lib/music/classes/Song.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { decode } from '@lavalink/encoding';
|
||||
import type { Track, TrackInfo } from '@lavaclient/types/v3';
|
||||
import * as MetadataFilter from 'metadata-filter';
|
||||
|
||||
export class Song implements TrackInfo {
|
||||
readonly track: string;
|
||||
requester?: RequesterInfo;
|
||||
length: number;
|
||||
identifier: string;
|
||||
author: string;
|
||||
isStream: boolean;
|
||||
position: number;
|
||||
title: string;
|
||||
uri: string;
|
||||
isSeekable: boolean;
|
||||
sourceName: string;
|
||||
thumbnail: string;
|
||||
added: number;
|
||||
|
||||
constructor(
|
||||
track: string | Track,
|
||||
added?: number,
|
||||
requester?: RequesterInfo
|
||||
) {
|
||||
this.track = typeof track === 'string' ? track : track.track;
|
||||
this.requester = requester;
|
||||
this.added = added ?? Date.now();
|
||||
const filterSet = {
|
||||
song: [
|
||||
MetadataFilter.removeVersion,
|
||||
MetadataFilter.removeRemastered,
|
||||
MetadataFilter.fixTrackSuffix,
|
||||
MetadataFilter.removeLive,
|
||||
MetadataFilter.youtube,
|
||||
MetadataFilter.normalizeFeature
|
||||
]
|
||||
};
|
||||
const filter = MetadataFilter.createFilter(filterSet);
|
||||
|
||||
// TODO: make this less shitty
|
||||
if (typeof track !== 'string') {
|
||||
this.length = track.info.length;
|
||||
this.identifier = track.info.identifier;
|
||||
this.author = track.info.author;
|
||||
this.isStream = track.info.isStream;
|
||||
this.position = track.info.position;
|
||||
this.title = filter.filterField('song', track.info.title);
|
||||
this.uri = track.info.uri;
|
||||
this.isSeekable = track.info.isSeekable;
|
||||
this.sourceName = track.info.sourceName;
|
||||
} else {
|
||||
const decoded = decode(this.track);
|
||||
this.length = Number(decoded.length);
|
||||
this.identifier = decoded.identifier;
|
||||
this.author = decoded.author;
|
||||
this.isStream = decoded.isStream;
|
||||
this.position = Number(decoded.position);
|
||||
this.title = filter.filterField('song', decoded.title);
|
||||
this.uri = decoded.uri!;
|
||||
this.isSeekable = !decoded.isStream;
|
||||
this.sourceName = decoded.source;
|
||||
}
|
||||
|
||||
// Thumbnails
|
||||
switch (this.sourceName) {
|
||||
case 'soundcloud': {
|
||||
this.thumbnail =
|
||||
'https://a-v2.sndcdn.com/assets/images/sc-icons/fluid-b4e7a64b8b.png'; // SoundCloud Logo
|
||||
break;
|
||||
}
|
||||
|
||||
case 'youtube': {
|
||||
this.thumbnail = `https://img.youtube.com/vi/${this.identifier}/hqdefault.jpg`; // Track Thumbnail
|
||||
break;
|
||||
}
|
||||
case 'twitch': {
|
||||
this.thumbnail = 'https://i.imgur.com/nO3f4jq.png'; // large Twitch Logo
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
this.thumbnail = 'https://cdn.discordapp.com/embed/avatars/1.png'; // Discord Default Avatar
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface RequesterInfo {
|
||||
avatar?: string | null;
|
||||
defaultAvatarURL?: string;
|
||||
id?: string;
|
||||
name?: string;
|
||||
}
|
||||
205
apps/bot/src/lib/music/nowPlayingEmbed.ts
Normal file
205
apps/bot/src/lib/music/nowPlayingEmbed.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
// import { container } from '@sapphire/framework';
|
||||
import { ColorResolvable, EmbedBuilder } from 'discord.js';
|
||||
import progressbar from 'string-progressbar';
|
||||
import type { Song } from './classes/Song';
|
||||
|
||||
type PositionType = number | undefined;
|
||||
|
||||
export class NowPlayingEmbed {
|
||||
track: Song;
|
||||
position: PositionType;
|
||||
length: number;
|
||||
volume: number;
|
||||
queue?: Song[];
|
||||
last?: Song;
|
||||
paused?: Boolean;
|
||||
|
||||
public constructor(
|
||||
track: Song,
|
||||
position: PositionType,
|
||||
length: number,
|
||||
volume: number,
|
||||
queue?: Song[],
|
||||
last?: Song,
|
||||
paused?: Boolean
|
||||
) {
|
||||
this.track = track;
|
||||
this.position = position;
|
||||
this.length = length;
|
||||
this.volume = volume;
|
||||
this.queue = queue;
|
||||
this.last = last;
|
||||
this.paused = paused;
|
||||
}
|
||||
|
||||
public async NowPlayingEmbed(): Promise<EmbedBuilder> {
|
||||
let trackLength = this.timeString(
|
||||
this.millisecondsToTimeObject(this.length)
|
||||
);
|
||||
|
||||
const durationText = this.track.isSeekable
|
||||
? `:stopwatch: ${trackLength}`
|
||||
: `:red_circle: Live Stream`;
|
||||
const userAvatar = this.track.requester?.avatar
|
||||
? `https://cdn.discordapp.com/avatars/${this.track.requester?.id}/${this.track.requester?.avatar}.png`
|
||||
: this.track.requester?.defaultAvatarURL ??
|
||||
'https://cdn.discordapp.com/embed/avatars/1.png'; // default Discord Avatar
|
||||
|
||||
let embedColor: ColorResolvable;
|
||||
let sourceTxt: string;
|
||||
let sourceIcon: string;
|
||||
let streamData;
|
||||
|
||||
switch (this.track.sourceName) {
|
||||
case 'soundcloud': {
|
||||
sourceTxt = 'SoundCloud';
|
||||
sourceIcon =
|
||||
'https://a-v2.sndcdn.com/assets/images/sc-icons/fluid-b4e7a64b8b.png';
|
||||
embedColor = '#F26F23';
|
||||
break;
|
||||
}
|
||||
|
||||
// case 'twitch': {
|
||||
// sourceTxt = 'Twitch';
|
||||
// sourceIcon =
|
||||
// 'https://static.twitchcdn.net/assets/favicon-32-e29e246c157142c94346.png';
|
||||
// embedColor = 'Purple';
|
||||
// const twitch = container.client.twitch;
|
||||
// if (twitch.auth.access_token) {
|
||||
// try {
|
||||
// streamData = await twitch.api.getStream({
|
||||
// login: this.track.author.toLowerCase(),
|
||||
// token: twitch.auth.access_token
|
||||
// });
|
||||
// } catch {
|
||||
// streamData = undefined;
|
||||
// }
|
||||
// }
|
||||
// break;
|
||||
// }
|
||||
|
||||
case 'youtube': {
|
||||
sourceTxt = 'YouTube';
|
||||
sourceIcon =
|
||||
'https://www.youtube.com/s/desktop/acce624e/img/favicon_32x32.png';
|
||||
embedColor = '#FF0000';
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
sourceTxt = 'Somewhere';
|
||||
sourceIcon = 'https://cdn.discordapp.com/embed/avatars/1.png';
|
||||
embedColor = 'DarkRed';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const vol = this.volume;
|
||||
let volumeIcon: string = ':speaker: ';
|
||||
if (vol > 50) volumeIcon = ':loud_sound: ';
|
||||
if (vol <= 50 && vol > 20) volumeIcon = ':sound: ';
|
||||
const embedFieldData = [
|
||||
{
|
||||
name: 'Volume',
|
||||
value: `${volumeIcon} ${this.volume}%`,
|
||||
inline: true
|
||||
},
|
||||
{ name: 'Duration', value: durationText, inline: true }
|
||||
];
|
||||
|
||||
if (this.queue?.length) {
|
||||
embedFieldData.push(
|
||||
{
|
||||
name: 'Queue',
|
||||
value: `:notes: ${this.queue.length} ${
|
||||
this.queue.length == 1 ? 'Song' : 'Songs'
|
||||
}`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'Next',
|
||||
value: `[${this.queue[0].title}](${this.queue[0].uri})`,
|
||||
inline: false
|
||||
}
|
||||
);
|
||||
}
|
||||
const baseEmbed = new EmbedBuilder()
|
||||
.setTitle(
|
||||
`${this.paused ? ':pause_button: ' : ':arrow_forward: '} ${
|
||||
this.track.title
|
||||
}`
|
||||
)
|
||||
.setAuthor({
|
||||
name: sourceTxt,
|
||||
iconURL: sourceIcon
|
||||
})
|
||||
.setURL(this.track.uri)
|
||||
.setThumbnail(this.track.thumbnail)
|
||||
.setColor(embedColor)
|
||||
.addFields(embedFieldData)
|
||||
.setTimestamp(this.track.added ?? Date.now())
|
||||
.setFooter({
|
||||
text: `Requested By ${this.track.requester?.name}`,
|
||||
iconURL: userAvatar
|
||||
});
|
||||
|
||||
if (!this.track.isSeekable || this.track.isStream) {
|
||||
if (streamData && this.track.sourceName == 'twitch') {
|
||||
const game = `[${
|
||||
streamData.game_name
|
||||
}](https://www.twitch.tv/directory/game/${encodeURIComponent(
|
||||
streamData.game_name
|
||||
)})`;
|
||||
const upTime = this.timeString(
|
||||
this.millisecondsToTimeObject(
|
||||
Date.now() - new Date(streamData.started_at).getTime()
|
||||
)
|
||||
);
|
||||
return baseEmbed
|
||||
.setDescription(
|
||||
`**Game**: ${game}\n**Viewers**: ${
|
||||
streamData.viewer_count
|
||||
}\n**Uptime**: ${upTime}\n **Started**: <t:${Math.floor(
|
||||
new Date(streamData.started_at).getTime() / 1000
|
||||
)}:t>`
|
||||
)
|
||||
.setImage(
|
||||
streamData.thumbnail_url.replace('{width}x{height}', '852x480') +
|
||||
`?${new Date(streamData.started_at).getTime()}`
|
||||
);
|
||||
} else return baseEmbed;
|
||||
}
|
||||
|
||||
// song just started embed
|
||||
if (this.position == undefined) this.position = 0;
|
||||
const bar = progressbar.splitBar(this.length, this.position, 22)[0];
|
||||
baseEmbed.setDescription(
|
||||
`${this.timeString(
|
||||
this.millisecondsToTimeObject(this.position)
|
||||
)} ${bar} ${trackLength}`
|
||||
);
|
||||
|
||||
return baseEmbed;
|
||||
}
|
||||
|
||||
private timeString(timeObject: any) {
|
||||
if (timeObject[1] === true) return timeObject[0];
|
||||
return `${timeObject.hours ? timeObject.hours + ':' : ''}${
|
||||
timeObject.minutes ? timeObject.minutes : '00'
|
||||
}:${
|
||||
timeObject.seconds < 10
|
||||
? '0' + timeObject.seconds
|
||||
: timeObject.seconds
|
||||
? timeObject.seconds
|
||||
: '00'
|
||||
}`;
|
||||
}
|
||||
|
||||
private millisecondsToTimeObject(milliseconds: number) {
|
||||
return {
|
||||
seconds: Math.floor((milliseconds / 1000) % 60),
|
||||
minutes: Math.floor((milliseconds / (1000 * 60)) % 60),
|
||||
hours: Math.floor((milliseconds / (1000 * 60 * 60)) % 24)
|
||||
};
|
||||
}
|
||||
}
|
||||
109
apps/bot/src/lib/music/searchSong.ts
Normal file
109
apps/bot/src/lib/music/searchSong.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { container } from '@sapphire/framework';
|
||||
import { SpotifyItemType } from '@lavaclient/spotify';
|
||||
import { Song } from './classes/Song';
|
||||
import type { User } from 'discord.js';
|
||||
|
||||
export default async function searchSong(
|
||||
query: string,
|
||||
user: User
|
||||
): Promise<[string, Song[]]> {
|
||||
const { client } = container;
|
||||
let tracks: Song[] = [];
|
||||
let response;
|
||||
let displayMessage = '';
|
||||
const { avatar, defaultAvatarURL, id, username } = user;
|
||||
|
||||
if (client.music.spotify.isSpotifyUrl(query)) {
|
||||
const item = await client.music.spotify.load(query);
|
||||
switch (item?.type) {
|
||||
case SpotifyItemType.Track:
|
||||
const track = await item.resolveYoutubeTrack();
|
||||
tracks = [
|
||||
new Song(track, Date.now(), {
|
||||
avatar,
|
||||
defaultAvatarURL,
|
||||
id,
|
||||
name: username
|
||||
})
|
||||
];
|
||||
displayMessage = `Queued track [**${item.name}**](${query}).`;
|
||||
break;
|
||||
case SpotifyItemType.Artist:
|
||||
response = await item.resolveYoutubeTracks();
|
||||
response.forEach(track =>
|
||||
tracks.push(
|
||||
new Song(track, Date.now(), {
|
||||
avatar,
|
||||
defaultAvatarURL,
|
||||
id,
|
||||
name: username
|
||||
})
|
||||
)
|
||||
);
|
||||
displayMessage = `Queued the **Top ${tracks.length} tracks** for [**${item.name}**](${query}).`;
|
||||
break;
|
||||
case SpotifyItemType.Album:
|
||||
case SpotifyItemType.Playlist:
|
||||
response = await item.resolveYoutubeTracks();
|
||||
response.forEach(track =>
|
||||
tracks.push(
|
||||
new Song(track, Date.now(), {
|
||||
avatar,
|
||||
defaultAvatarURL,
|
||||
id,
|
||||
name: username
|
||||
})
|
||||
)
|
||||
);
|
||||
displayMessage = `Queued **${
|
||||
tracks.length
|
||||
} tracks** from ${SpotifyItemType[item.type].toLowerCase()} [**${
|
||||
item.name
|
||||
}**](${query}).`;
|
||||
break;
|
||||
default:
|
||||
displayMessage = ":x: Couldn't find what you were looking for :(";
|
||||
return [displayMessage, tracks];
|
||||
}
|
||||
return [displayMessage, tracks];
|
||||
} else {
|
||||
const results = await client.music.rest.loadTracks(
|
||||
/^https?:\/\//.test(query) ? query : `ytsearch:${query}`
|
||||
);
|
||||
|
||||
switch (results.loadType) {
|
||||
case 'LOAD_FAILED':
|
||||
case 'NO_MATCHES':
|
||||
displayMessage = ":x: Couldn't find what you were looking for :(";
|
||||
return [displayMessage, tracks];
|
||||
case 'PLAYLIST_LOADED':
|
||||
results.tracks.forEach((track: any) =>
|
||||
tracks.push(
|
||||
new Song(track, Date.now(), {
|
||||
avatar,
|
||||
defaultAvatarURL,
|
||||
id,
|
||||
name: username
|
||||
})
|
||||
)
|
||||
);
|
||||
displayMessage = `Queued playlist [**${results.playlistInfo.name}**](${query}), it has a total of **${tracks.length}** tracks.`;
|
||||
break;
|
||||
case 'TRACK_LOADED':
|
||||
case 'SEARCH_RESULT':
|
||||
const [track] = results.tracks;
|
||||
tracks = [
|
||||
new Song(track, Date.now(), {
|
||||
avatar,
|
||||
defaultAvatarURL,
|
||||
id,
|
||||
name: username
|
||||
})
|
||||
];
|
||||
displayMessage = `Queued [**${track.info.title}**](${track.info.uri})`;
|
||||
break;
|
||||
}
|
||||
|
||||
return [displayMessage, tracks];
|
||||
}
|
||||
}
|
||||
20
apps/bot/src/lib/setup.ts
Normal file
20
apps/bot/src/lib/setup.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import {
|
||||
ApplicationCommandRegistries,
|
||||
RegisterBehavior
|
||||
} from '@sapphire/framework';
|
||||
import '@sapphire/plugin-api/register';
|
||||
import '@sapphire/plugin-editable-commands/register';
|
||||
import '@sapphire/plugin-subcommands/register';
|
||||
import * as colorette from 'colorette';
|
||||
import { inspect } from 'util';
|
||||
|
||||
// Set default behavior to bulk overwrite
|
||||
ApplicationCommandRegistries.setDefaultBehaviorWhenNotIdentical(
|
||||
RegisterBehavior.BulkOverwrite
|
||||
);
|
||||
|
||||
// Set default inspection depth
|
||||
inspect.defaultOptions.depth = 1;
|
||||
|
||||
// Enable colorette
|
||||
colorette.createColors({ useColor: true });
|
||||
79
apps/bot/src/lib/structures/ExtendedClient.ts
Normal file
79
apps/bot/src/lib/structures/ExtendedClient.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { SapphireClient } from '@sapphire/framework';
|
||||
import '@sapphire/plugin-hmr/register';
|
||||
import { QueueClient } from '../music/classes/QueueClient';
|
||||
import Redis from 'ioredis';
|
||||
import { GatewayDispatchEvents, IntentsBitField } from 'discord.js';
|
||||
import { deletePlayerEmbed } from '../music/buttonsCollector';
|
||||
|
||||
export class ExtendedClient extends SapphireClient {
|
||||
readonly music: QueueClient;
|
||||
leaveTimers: { [key: string]: NodeJS.Timer };
|
||||
public constructor() {
|
||||
super({
|
||||
intents: [
|
||||
IntentsBitField.Flags.Guilds,
|
||||
IntentsBitField.Flags.GuildMembers,
|
||||
IntentsBitField.Flags.GuildMessages,
|
||||
IntentsBitField.Flags.GuildMessageReactions,
|
||||
IntentsBitField.Flags.GuildVoiceStates
|
||||
],
|
||||
logger: { level: 100 },
|
||||
loadMessageCommandListeners: true,
|
||||
hmr: {
|
||||
enabled: process.env.NODE_ENV === 'development'
|
||||
}
|
||||
});
|
||||
|
||||
this.music = new QueueClient({
|
||||
sendGatewayPayload: (id, payload) =>
|
||||
this.guilds.cache.get(id)?.shard?.send(payload),
|
||||
options: {
|
||||
redis: new Redis({
|
||||
host: process.env.REDIS_HOST || 'localhost',
|
||||
port: Number.parseInt(process.env.REDIS_PORT!) || 6379,
|
||||
password: process.env.REDIS_PASSWORD || '',
|
||||
db: Number.parseInt(process.env.REDIS_DB!) || 0
|
||||
})
|
||||
},
|
||||
connection: {
|
||||
host: process.env.LAVA_HOST || '',
|
||||
password: process.env.LAVA_PASS || '',
|
||||
port: process.env.LAVA_PORT ? +process.env.LAVA_PORT : 1339,
|
||||
secure: process.env.LAVA_SECURE === 'true' ? true : false
|
||||
}
|
||||
});
|
||||
|
||||
this.ws.on(GatewayDispatchEvents.VoiceServerUpdate, async data => {
|
||||
await this.music.handleVoiceUpdate(data);
|
||||
});
|
||||
|
||||
this.ws.on(GatewayDispatchEvents.VoiceStateUpdate, async data => {
|
||||
// handle if a mod right-clicks disconnect on the bot
|
||||
if (!data.channel_id && data.user_id == this.application?.id) {
|
||||
const queue = this.music.queues.get(data.guild_id);
|
||||
await deletePlayerEmbed(queue);
|
||||
await queue.clear();
|
||||
queue.destroyPlayer();
|
||||
}
|
||||
await this.music.handleVoiceUpdate(data);
|
||||
});
|
||||
|
||||
this.leaveTimers = {};
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@sapphire/framework' {
|
||||
interface SapphireClient {
|
||||
readonly music: QueueClient;
|
||||
leaveTimers: { [key: string]: NodeJS.Timer };
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'lavaclient' {
|
||||
interface Player {
|
||||
nightcore: boolean;
|
||||
vaporwave: boolean;
|
||||
karaoke: boolean;
|
||||
bassboost: boolean;
|
||||
}
|
||||
}
|
||||
24
apps/bot/src/listeners/commandDenied.ts
Normal file
24
apps/bot/src/listeners/commandDenied.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import {
|
||||
type ChatInputCommandDeniedPayload,
|
||||
Listener,
|
||||
type ListenerOptions,
|
||||
UserError
|
||||
} from '@sapphire/framework';
|
||||
|
||||
@ApplyOptions<ListenerOptions>({
|
||||
name: 'chatInputCommandDenied'
|
||||
})
|
||||
export class CommandDeniedListener extends Listener {
|
||||
public override async run(
|
||||
{ context, message: content }: UserError,
|
||||
{ interaction }: ChatInputCommandDeniedPayload
|
||||
): Promise<void> {
|
||||
await interaction.reply({
|
||||
ephemeral: true,
|
||||
content: content
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
24
apps/bot/src/listeners/guild/guildCreate.ts
Normal file
24
apps/bot/src/listeners/guild/guildCreate.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import { Listener, type ListenerOptions } from '@sapphire/framework';
|
||||
import type { Guild } from 'discord.js';
|
||||
import { trpcNode } from '../../trpc';
|
||||
|
||||
@ApplyOptions<ListenerOptions>({
|
||||
name: 'guildCreate'
|
||||
})
|
||||
export class GuildCreateListener extends Listener {
|
||||
public override async run(guild: Guild): Promise<void> {
|
||||
const owner = await guild.fetchOwner();
|
||||
|
||||
await trpcNode.user.create.mutate({
|
||||
id: owner.id,
|
||||
name: owner.user.username
|
||||
});
|
||||
|
||||
await trpcNode.guild.create.mutate({
|
||||
id: guild.id,
|
||||
name: guild.name,
|
||||
ownerId: owner.id
|
||||
});
|
||||
}
|
||||
}
|
||||
15
apps/bot/src/listeners/guild/guildDelete.ts
Normal file
15
apps/bot/src/listeners/guild/guildDelete.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import { Listener, type ListenerOptions } from '@sapphire/framework';
|
||||
import type { Guild } from 'discord.js';
|
||||
import { trpcNode } from '../../trpc';
|
||||
|
||||
@ApplyOptions<ListenerOptions>({
|
||||
name: 'guildDelete'
|
||||
})
|
||||
export class GuildDeleteListener extends Listener {
|
||||
public override async run(guild: Guild): Promise<void> {
|
||||
await trpcNode.guild.delete.mutate({
|
||||
id: guild.id
|
||||
});
|
||||
}
|
||||
}
|
||||
38
apps/bot/src/listeners/guild/guildMemberAdd.ts
Normal file
38
apps/bot/src/listeners/guild/guildMemberAdd.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
//import type { Guild } from '@prisma/client';
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import { Listener, type ListenerOptions } from '@sapphire/framework';
|
||||
import type { GuildMember, TextChannel } from 'discord.js';
|
||||
import { trpcNode } from '../../trpc';
|
||||
|
||||
@ApplyOptions<ListenerOptions>({
|
||||
name: 'guildMemberAdd'
|
||||
})
|
||||
export class GuildMemberListener extends Listener {
|
||||
public override async run(member: GuildMember): Promise<void> {
|
||||
const guildQuery = await trpcNode.guild.getGuild.query({
|
||||
id: member.guild.id
|
||||
});
|
||||
|
||||
if (!guildQuery || !guildQuery.guild) return;
|
||||
|
||||
const { welcomeMessage, welcomeMessageEnabled, welcomeMessageChannel } =
|
||||
guildQuery.guild;
|
||||
|
||||
if (
|
||||
!welcomeMessageEnabled ||
|
||||
!welcomeMessage ||
|
||||
!welcomeMessage.length ||
|
||||
!welcomeMessageChannel
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const channel = (await member.guild.channels.fetch(
|
||||
welcomeMessageChannel
|
||||
)) as TextChannel;
|
||||
|
||||
if (channel) {
|
||||
await channel.send({ content: `@${member.id} ${welcomeMessage}` });
|
||||
}
|
||||
}
|
||||
}
|
||||
25
apps/bot/src/listeners/music/musicFinish.ts
Normal file
25
apps/bot/src/listeners/music/musicFinish.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import { Listener, type ListenerOptions, container } from '@sapphire/framework';
|
||||
import { deletePlayerEmbed } from '../../lib/music/buttonsCollector';
|
||||
import type { Queue } from '../../lib/music/classes/Queue';
|
||||
// import { inactivityTime } from '../../lib/music/handleOptions';
|
||||
|
||||
@ApplyOptions<ListenerOptions>({
|
||||
name: 'musicFinish'
|
||||
})
|
||||
export class MusicFinishListener extends Listener {
|
||||
public override async run(
|
||||
queue: Queue,
|
||||
skipped: boolean = false
|
||||
): Promise<void> {
|
||||
const channel = await queue.getTextChannel();
|
||||
const { client } = container;
|
||||
await deletePlayerEmbed(queue);
|
||||
if (skipped) return;
|
||||
client.leaveTimers[queue.player.guildId] = setTimeout(async () => {
|
||||
if (channel) queue.client.emit('musicFinishNotify', channel);
|
||||
await queue.leave();
|
||||
// }, inactivityTime());
|
||||
}, 30000);
|
||||
}
|
||||
}
|
||||
12
apps/bot/src/listeners/music/musicFinishNotify.ts
Normal file
12
apps/bot/src/listeners/music/musicFinishNotify.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import { Listener, type ListenerOptions } from '@sapphire/framework';
|
||||
import type { TextChannel } from 'discord.js';
|
||||
|
||||
@ApplyOptions<ListenerOptions>({
|
||||
name: 'musicFinishNotify'
|
||||
})
|
||||
export class MusicFinishNotifyListener extends Listener {
|
||||
public override async run(channel: TextChannel): Promise<void> {
|
||||
await channel.send({ content: ':zzz: Leaving due to inactivity' });
|
||||
}
|
||||
}
|
||||
15
apps/bot/src/listeners/music/musicSongPause.ts
Normal file
15
apps/bot/src/listeners/music/musicSongPause.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import { Listener, type ListenerOptions } from '@sapphire/framework';
|
||||
import type { ChatInputCommandInteraction } from 'discord.js';
|
||||
|
||||
@ApplyOptions<ListenerOptions>({
|
||||
name: 'musicSongPause'
|
||||
})
|
||||
export class MusicSongPauseListener extends Listener {
|
||||
public override async run(
|
||||
interaction: ChatInputCommandInteraction
|
||||
): Promise<void> {
|
||||
await interaction.reply({ content: `Track paused.` });
|
||||
return;
|
||||
}
|
||||
}
|
||||
26
apps/bot/src/listeners/music/musicSongPlay.ts
Normal file
26
apps/bot/src/listeners/music/musicSongPlay.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import { container, Listener, type ListenerOptions } from '@sapphire/framework';
|
||||
import type { Queue } from '../../lib/music/classes/Queue';
|
||||
import type { Song } from '../../lib/music/classes/Song';
|
||||
|
||||
@ApplyOptions<ListenerOptions>({
|
||||
name: 'musicSongPlay'
|
||||
})
|
||||
export class MusicSongPlayListener extends Listener {
|
||||
public override async run(queue: Queue, track: Song): Promise<void> {
|
||||
const channel = await queue.getTextChannel();
|
||||
if (channel) {
|
||||
const { client } = container;
|
||||
|
||||
clearTimeout(client.leaveTimers[queue.player.guildId]);
|
||||
delete client.leaveTimers[queue.player.guildId];
|
||||
// Leave Voice Channel when attempting to stream to an empty channel
|
||||
|
||||
if (channel.guild.members.me?.voice.channel?.members.size == 1) {
|
||||
await queue.leave();
|
||||
return;
|
||||
}
|
||||
queue.client.emit('musicSongPlayMessage', channel, track);
|
||||
}
|
||||
}
|
||||
}
|
||||
37
apps/bot/src/listeners/music/musicSongPlayMessage.ts
Normal file
37
apps/bot/src/listeners/music/musicSongPlayMessage.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import { container, Listener, type ListenerOptions } from '@sapphire/framework';
|
||||
import type { TextChannel } from 'discord.js';
|
||||
import { embedButtons } from '../../lib/music/buttonHandler';
|
||||
import { NowPlayingEmbed } from '../../lib/music/nowPlayingEmbed';
|
||||
import type { Song } from '../../lib/music/classes/Song';
|
||||
import { manageStageChannel } from '../../lib/music/channelHandler';
|
||||
|
||||
@ApplyOptions<ListenerOptions>({
|
||||
name: 'musicSongPlayMessage'
|
||||
})
|
||||
export class MusicSongPlayMessageListener extends Listener {
|
||||
public override async run(channel: TextChannel, track: Song): Promise<void> {
|
||||
const { client } = container;
|
||||
const queue = client.music.queues.get(channel.guild.id);
|
||||
const tracks = await queue.tracks();
|
||||
const NowPlaying = new NowPlayingEmbed(
|
||||
track,
|
||||
queue.player.accuratePosition,
|
||||
track.length ?? 0,
|
||||
queue.player.volume,
|
||||
tracks,
|
||||
tracks.at(-1),
|
||||
queue.paused
|
||||
);
|
||||
try {
|
||||
await manageStageChannel(
|
||||
channel.guild.members.me?.voice.channel!,
|
||||
channel.guild.members.me!,
|
||||
queue
|
||||
);
|
||||
await embedButtons(await NowPlaying.NowPlayingEmbed(), queue, track);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
14
apps/bot/src/listeners/music/musicSongResume.ts
Normal file
14
apps/bot/src/listeners/music/musicSongResume.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import { Listener, type ListenerOptions } from '@sapphire/framework';
|
||||
import type { ChatInputCommandInteraction } from 'discord.js';
|
||||
|
||||
@ApplyOptions<ListenerOptions>({
|
||||
name: 'musicSongResume'
|
||||
})
|
||||
export class MusicSongResumeListener extends Listener {
|
||||
public override async run(
|
||||
interaction: ChatInputCommandInteraction
|
||||
): Promise<void> {
|
||||
await interaction.reply({ content: `Track resumed.` });
|
||||
}
|
||||
}
|
||||
17
apps/bot/src/listeners/music/musicSongSkipNotify.ts
Normal file
17
apps/bot/src/listeners/music/musicSongSkipNotify.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { Song } from '../../lib/music/classes/Song';
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import { Listener, type ListenerOptions } from '@sapphire/framework';
|
||||
import { ChatInputCommandInteraction } from 'discord.js';
|
||||
|
||||
@ApplyOptions<ListenerOptions>({
|
||||
name: 'musicSongSkipNotify'
|
||||
})
|
||||
export class MusicSongSkipNotifyListener extends Listener {
|
||||
public override async run(
|
||||
interaction: ChatInputCommandInteraction,
|
||||
track: Song
|
||||
): Promise<void> {
|
||||
if (!track) return;
|
||||
await interaction.reply({ content: `${track.title} has been skipped.` });
|
||||
}
|
||||
}
|
||||
104
apps/bot/src/listeners/tempchannels/voiceStateUpdate.ts
Normal file
104
apps/bot/src/listeners/tempchannels/voiceStateUpdate.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import { Listener, ListenerOptions } from '@sapphire/framework';
|
||||
import type { VoiceChannel, VoiceState } from 'discord.js';
|
||||
import { trpcNode } from '../../trpc';
|
||||
import { ChannelType } from 'discord.js';
|
||||
|
||||
@ApplyOptions<ListenerOptions>({
|
||||
name: 'voiceStateUpdate'
|
||||
})
|
||||
export class VoiceStateUpdateListener extends Listener {
|
||||
public override async run(
|
||||
oldState: VoiceState,
|
||||
newState: VoiceState
|
||||
): Promise<void> {
|
||||
const { guild: guildDB } = await trpcNode.guild.getGuild.query({
|
||||
id: newState.guild.id
|
||||
});
|
||||
|
||||
// now user is in hub channel, create him a new voice channel and move him there
|
||||
if (newState.channelId) {
|
||||
if (!newState.member) return; // should not happen but just in case
|
||||
|
||||
if (newState.channelId === guildDB?.hubChannel && guildDB.hub) {
|
||||
const { tempChannel } = await trpcNode.hub.getTempChannel.query({
|
||||
guildId: newState.guild.id,
|
||||
ownerId: newState.member.id
|
||||
});
|
||||
// user entered hub channel but he already has a temp channel, so move him there
|
||||
if (tempChannel) {
|
||||
await newState.setChannel(tempChannel.id);
|
||||
return;
|
||||
}
|
||||
|
||||
const guild = newState.guild;
|
||||
const channels = guild.channels;
|
||||
|
||||
const channel = await channels.create({
|
||||
name: `${newState.member.user.username}'s channel`,
|
||||
type: ChannelType.GuildVoice,
|
||||
parent: guildDB?.hub,
|
||||
permissionOverwrites: [
|
||||
{
|
||||
id: newState.member.id,
|
||||
allow: [
|
||||
'MoveMembers',
|
||||
'MuteMembers',
|
||||
'DeafenMembers',
|
||||
'ManageChannels',
|
||||
'Stream'
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
await trpcNode.hub.createTempChannel.mutate({
|
||||
guildId: newState.guild.id,
|
||||
ownerId: newState.member.id,
|
||||
channelId: channel.id
|
||||
});
|
||||
|
||||
await newState.member.voice.setChannel(channel);
|
||||
} else {
|
||||
const { tempChannel } = await trpcNode.hub.getTempChannel.query({
|
||||
guildId: newState.guild.id,
|
||||
ownerId: newState.member.id
|
||||
});
|
||||
if (!tempChannel) return;
|
||||
|
||||
if (tempChannel.id === newState.channelId) return;
|
||||
|
||||
const channel = (await newState.guild.channels.fetch(
|
||||
tempChannel.id
|
||||
)) as VoiceChannel;
|
||||
if (!channel) return;
|
||||
|
||||
Promise.all([
|
||||
channel.delete(),
|
||||
trpcNode.hub.deleteTempChannel.mutate({
|
||||
channelId: tempChannel.id
|
||||
})
|
||||
]);
|
||||
}
|
||||
} else if (!newState.channelId) {
|
||||
// user left hub channel, delete his temp channel
|
||||
deleteChannel(oldState);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteChannel(state: VoiceState) {
|
||||
const { tempChannel } = await trpcNode.hub.getTempChannel.query({
|
||||
guildId: state.guild.id,
|
||||
ownerId: state.member!.id
|
||||
});
|
||||
|
||||
if (tempChannel) {
|
||||
Promise.all([
|
||||
state.channel?.delete(),
|
||||
trpcNode.hub.deleteTempChannel.mutate({
|
||||
channelId: tempChannel.id
|
||||
})
|
||||
]);
|
||||
}
|
||||
}
|
||||
43
apps/bot/src/preconditions/inPlayerVoiceChannel.ts
Normal file
43
apps/bot/src/preconditions/inPlayerVoiceChannel.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import {
|
||||
Precondition,
|
||||
type PreconditionOptions,
|
||||
type PreconditionResult
|
||||
} from '@sapphire/framework';
|
||||
import type {
|
||||
ChatInputCommandInteraction,
|
||||
GuildMember,
|
||||
VoiceBasedChannel
|
||||
} from 'discord.js';
|
||||
import { container } from '@sapphire/framework';
|
||||
|
||||
@ApplyOptions<PreconditionOptions>({
|
||||
name: 'inPlayerVoiceChannel'
|
||||
})
|
||||
export class inPlayerVoiceChannel extends Precondition {
|
||||
public override chatInputRun(
|
||||
interaction: ChatInputCommandInteraction
|
||||
): PreconditionResult {
|
||||
const member = interaction.member as GuildMember;
|
||||
// this precondition comes after a precondition that makes sure the user is in a voice channel
|
||||
const voiceChannel = member.voice!.channel as VoiceBasedChannel;
|
||||
|
||||
const { client } = container;
|
||||
const queue = client.music.queues.get(interaction.guildId!);
|
||||
|
||||
const queueVoiceChannel = queue.voiceChannel;
|
||||
if (queueVoiceChannel && queueVoiceChannel.id !== voiceChannel.id) {
|
||||
return this.error({
|
||||
message: `You're in the wrong channel! Join <#${queueVoiceChannel?.id}>`
|
||||
});
|
||||
}
|
||||
|
||||
return this.ok();
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@sapphire/framework' {
|
||||
export interface Preconditions {
|
||||
inPlayerVoiceChannel: never;
|
||||
}
|
||||
}
|
||||
32
apps/bot/src/preconditions/inVoiceChannel.ts
Normal file
32
apps/bot/src/preconditions/inVoiceChannel.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import {
|
||||
Precondition,
|
||||
PreconditionOptions,
|
||||
PreconditionResult
|
||||
} from '@sapphire/framework';
|
||||
import type { ChatInputCommandInteraction, GuildMember } from 'discord.js';
|
||||
|
||||
@ApplyOptions<PreconditionOptions>({
|
||||
name: 'inVoiceChannel'
|
||||
})
|
||||
export class inVoiceChannel extends Precondition {
|
||||
public override chatInputRun(
|
||||
interaction: ChatInputCommandInteraction
|
||||
): PreconditionResult {
|
||||
const member = interaction.member as GuildMember;
|
||||
const voiceChannel = member!.voice!.channel;
|
||||
|
||||
if (!voiceChannel) {
|
||||
return this.error({
|
||||
message: 'You must be in a voice channel in order to use this command!'
|
||||
});
|
||||
}
|
||||
return this.ok();
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@sapphire/framework' {
|
||||
export interface Preconditions {
|
||||
inVoiceChannel: never;
|
||||
}
|
||||
}
|
||||
41
apps/bot/src/preconditions/isCommandDisabled.ts
Normal file
41
apps/bot/src/preconditions/isCommandDisabled.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import {
|
||||
AsyncPreconditionResult,
|
||||
Precondition,
|
||||
PreconditionOptions
|
||||
} from '@sapphire/framework';
|
||||
import { ChatInputCommandInteraction } from 'discord.js';
|
||||
import { trpcNode } from '../trpc';
|
||||
|
||||
@ApplyOptions<PreconditionOptions>({
|
||||
name: 'isCommandDisabled'
|
||||
})
|
||||
export class IsCommandDisabledPrecondition extends Precondition {
|
||||
public override async chatInputRun(
|
||||
interaction: ChatInputCommandInteraction
|
||||
): AsyncPreconditionResult {
|
||||
const commandID = interaction.commandId;
|
||||
const guildID = interaction.guildId as string;
|
||||
// Most likly a DM
|
||||
if (!interaction.guildId && interaction.user.id) {
|
||||
return this.ok();
|
||||
}
|
||||
const data = await trpcNode.command.getDisabledCommands.query({
|
||||
guildId: guildID
|
||||
});
|
||||
|
||||
if (data.disabledCommands.includes(commandID)) {
|
||||
return this.error({
|
||||
message: 'This command is disabled'
|
||||
});
|
||||
}
|
||||
|
||||
return this.ok();
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@sapphire/framework' {
|
||||
export interface Preconditions {
|
||||
isCommandDisabled: never;
|
||||
}
|
||||
}
|
||||
31
apps/bot/src/preconditions/playerIsPlaying.ts
Normal file
31
apps/bot/src/preconditions/playerIsPlaying.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import {
|
||||
Precondition,
|
||||
PreconditionOptions,
|
||||
PreconditionResult
|
||||
} from '@sapphire/framework';
|
||||
import { container } from '@sapphire/framework';
|
||||
import { ChatInputCommandInteraction } from 'discord.js';
|
||||
|
||||
@ApplyOptions<PreconditionOptions>({
|
||||
name: 'playerIsPlaying'
|
||||
})
|
||||
export class PlayerIsPlaying extends Precondition {
|
||||
public override chatInputRun(
|
||||
interaction: ChatInputCommandInteraction
|
||||
): PreconditionResult {
|
||||
const { client } = container;
|
||||
const player = client.music.players.get(interaction.guildId as string);
|
||||
|
||||
if (!player) {
|
||||
return this.error({ message: 'There is nothing playing at the moment!' });
|
||||
}
|
||||
return this.ok();
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@sapphire/framework' {
|
||||
export interface Preconditions {
|
||||
playerIsPlaying: never;
|
||||
}
|
||||
}
|
||||
38
apps/bot/src/preconditions/playlistExists.ts
Normal file
38
apps/bot/src/preconditions/playlistExists.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import {
|
||||
AsyncPreconditionResult,
|
||||
Precondition,
|
||||
PreconditionOptions
|
||||
} from '@sapphire/framework';
|
||||
import type { ChatInputCommandInteraction, GuildMember } from 'discord.js';
|
||||
import { trpcNode } from '../trpc';
|
||||
|
||||
@ApplyOptions<PreconditionOptions>({
|
||||
name: 'playlistExists'
|
||||
})
|
||||
export class PlaylistExists extends Precondition {
|
||||
public override async chatInputRun(
|
||||
interaction: ChatInputCommandInteraction
|
||||
): AsyncPreconditionResult {
|
||||
const playlistName = interaction.options.getString('playlist-name', true);
|
||||
|
||||
const guildMember = interaction.member as GuildMember;
|
||||
|
||||
const playlist = await trpcNode.playlist.getPlaylist.query({
|
||||
name: playlistName,
|
||||
userId: guildMember.id
|
||||
});
|
||||
|
||||
return playlist
|
||||
? this.ok()
|
||||
: this.error({
|
||||
message: `You have no playlist named **${playlistName}**`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@sapphire/framework' {
|
||||
export interface Preconditions {
|
||||
playlistExists: never;
|
||||
}
|
||||
}
|
||||
42
apps/bot/src/preconditions/playlistNotDuplicate.ts
Normal file
42
apps/bot/src/preconditions/playlistNotDuplicate.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import {
|
||||
AsyncPreconditionResult,
|
||||
Precondition,
|
||||
PreconditionOptions
|
||||
} from '@sapphire/framework';
|
||||
import type { ChatInputCommandInteraction, GuildMember } from 'discord.js';
|
||||
import { trpcNode } from '../trpc';
|
||||
|
||||
@ApplyOptions<PreconditionOptions>({
|
||||
name: 'playlistNotDuplicate'
|
||||
})
|
||||
export class PlaylistNotDuplicate extends Precondition {
|
||||
public override async chatInputRun(
|
||||
interaction: ChatInputCommandInteraction
|
||||
): AsyncPreconditionResult {
|
||||
const playlistName = interaction.options.getString('playlist-name', true);
|
||||
|
||||
const guildMember = interaction.member as GuildMember;
|
||||
|
||||
try {
|
||||
const playlist = await trpcNode.playlist.getPlaylist.query({
|
||||
name: playlistName,
|
||||
userId: guildMember.id
|
||||
});
|
||||
|
||||
if (playlist) throw new Error();
|
||||
} catch {
|
||||
return this.error({
|
||||
message: `There is already a playlist named **${playlistName}** in your saved playlists!`
|
||||
});
|
||||
}
|
||||
|
||||
return this.ok();
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@sapphire/framework' {
|
||||
export interface Preconditions {
|
||||
playlistNotDuplicate: never;
|
||||
}
|
||||
}
|
||||
40
apps/bot/src/preconditions/userInDB.ts
Normal file
40
apps/bot/src/preconditions/userInDB.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import {
|
||||
AsyncPreconditionResult,
|
||||
Precondition,
|
||||
PreconditionOptions
|
||||
} from '@sapphire/framework';
|
||||
import type { ChatInputCommandInteraction, GuildMember } from 'discord.js';
|
||||
import { trpcNode } from '../trpc';
|
||||
import Logger from '../lib/logger';
|
||||
|
||||
@ApplyOptions<PreconditionOptions>({
|
||||
name: 'userInDB'
|
||||
})
|
||||
export class UserInDB extends Precondition {
|
||||
public override async chatInputRun(
|
||||
interaction: ChatInputCommandInteraction
|
||||
): AsyncPreconditionResult {
|
||||
const guildMember = interaction.member as GuildMember;
|
||||
|
||||
try {
|
||||
const user = await trpcNode.user.create.mutate({
|
||||
id: guildMember.id,
|
||||
name: guildMember.user.username
|
||||
});
|
||||
|
||||
if (!user) throw new Error();
|
||||
} catch (error) {
|
||||
Logger.error(error);
|
||||
return this.error({ message: 'Something went wrong!' });
|
||||
}
|
||||
|
||||
return this.ok();
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@sapphire/framework' {
|
||||
export interface Preconditions {
|
||||
userInDB: never;
|
||||
}
|
||||
}
|
||||
31
apps/bot/src/preconditions/validateLanguageCode.ts
Normal file
31
apps/bot/src/preconditions/validateLanguageCode.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { ApplyOptions } from '@sapphire/decorators';
|
||||
import {
|
||||
Precondition,
|
||||
PreconditionOptions,
|
||||
PreconditionResult
|
||||
} from '@sapphire/framework';
|
||||
import { ChatInputCommandInteraction } from 'discord.js';
|
||||
import ISO6391 from 'iso-639-1';
|
||||
|
||||
@ApplyOptions<PreconditionOptions>({
|
||||
name: 'validateLanguageCode'
|
||||
})
|
||||
export class ValidLanguageCode extends Precondition {
|
||||
public override chatInputRun(
|
||||
interaction: ChatInputCommandInteraction
|
||||
): PreconditionResult {
|
||||
const targetLang = interaction.options.getString('target', true);
|
||||
const languageCode = ISO6391.getCode(targetLang);
|
||||
|
||||
if (!languageCode) {
|
||||
return this.error({ message: ':x: Please enter a valid language!' });
|
||||
}
|
||||
return this.ok();
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@sapphire/framework' {
|
||||
export interface Preconditions {
|
||||
validateLanguageCode: never;
|
||||
}
|
||||
}
|
||||
25
apps/bot/src/trpc.ts
Normal file
25
apps/bot/src/trpc.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { AppRouter } from '@master-bot/api/index';
|
||||
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client';
|
||||
import superjson from 'superjson';
|
||||
// @ts-ignore
|
||||
import * as trpcServer from '@trpc/server';
|
||||
// @ts-ignore
|
||||
import * as PrismaClient from '@prisma/client';
|
||||
const _importDynamic = new Function('modulePath', 'return import(modulePath)');
|
||||
|
||||
const fetch = async function (...args: any) {
|
||||
const { default: fetch } = await _importDynamic('node-fetch');
|
||||
return fetch(...args);
|
||||
};
|
||||
|
||||
const globalAny = global as any;
|
||||
globalAny.fetch = fetch;
|
||||
|
||||
export const trpcNode = createTRPCProxyClient<AppRouter>({
|
||||
links: [
|
||||
httpBatchLink({
|
||||
url: 'http://localhost:3000/api/trpc'
|
||||
})
|
||||
],
|
||||
transformer: superjson
|
||||
});
|
||||
20
apps/bot/tsconfig.json
Normal file
20
apps/bot/tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"extends": "@sapphire/ts-config",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"experimentalDecorators": true,
|
||||
"incremental": true,
|
||||
"outDir": "dist",
|
||||
"strict": true,
|
||||
"tsBuildInfoFile": "dist/.tsbuildinfo",
|
||||
"resolveJsonModule": true,
|
||||
"noUnusedParameters": false,
|
||||
"skipLibCheck": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"verbatimModuleSyntax": false,
|
||||
"esModuleInterop": true,
|
||||
"noImplicitAny": false
|
||||
},
|
||||
"include": ["src", "scripts", "src/env.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user