Skip to content

Commit

Permalink
3.7.0-dev-2
Browse files Browse the repository at this point in the history
Added commands /pl-remove, /pl-add, /pl-display
  • Loading branch information
AlexInCube committed Aug 19, 2024
1 parent d1ba342 commit 464f488
Show file tree
Hide file tree
Showing 10 changed files with 315 additions and 46 deletions.
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,13 @@ Cool audiobot for Discord created by <a href="https://vk.com/alexincube"><b>@Ale
## 🌟 Features
- Command /alcotest which shows your alcohol count in blood
- Audioplayer based on [Distube](https://github.com/skick1234/DisTube) with buttons
- Playlists for songs
- Lyrics for songs
- Downloading songs via /download command

![play-audioplayer](/wiki/images/commands/play-audioplayer.png)

- Support YouTube, Spotify, Soundcloud, Apple Music, any HTTP-stream and Discord Attachments (/playfile support MP3/WAV/OGG)
- Support Slash and Text commands (with customizable prefix per server using /setprefix)
- Support Slash and Text commands system (with customizable prefix per server using /setprefix)
- Localization (English and Russian are currently supported)
- Go to [Wiki](https://github.com/AlexInCube/AlCoTest/wiki) to get more information about features and other.
- Go to [Wiki](https://github.com/AlexInCube/AlCoTest/wiki) to get more information about features, commands, and others.
2 changes: 1 addition & 1 deletion src/EnvironmentVariables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ const envVariables = z.object({

BOT_MAX_SONGS_IN_QUEUE: z.coerce.number().positive().min(1).optional().default(500),
BOT_MAX_SONGS_HISTORY_SIZE: z.coerce.number().nonnegative().optional().default(60),
BOT_MAX_PLAYLISTS_PER_USER: z.coerce.number().positive().min(1).optional().default(25),
BOT_MAX_PLAYLISTS_PER_USER: z.coerce.number().positive().min(1).max(50).optional().default(25),
BOT_MAX_SONGS_IN_USER_PLAYLIST: z.coerce.number().positive().min(1).optional().default(500),

MONGO_URI: z.string(),
Expand Down
127 changes: 118 additions & 9 deletions src/commands/audio/pl-add.command.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,37 @@
import { CommandArgument, ICommand } from '../../CommandTypes.js';
import { GroupAudio } from './AudioTypes.js';
import { Message, PermissionsBitField, SlashCommandBuilder } from 'discord.js';
import { ChatInputCommandInteraction, Message, PermissionsBitField, SlashCommandBuilder, User } from 'discord.js';
import i18next from 'i18next';
import { UserPlaylistNamesAutocomplete } from '../../schemas/SchemaPlaylist.js';
import {
PlaylistIsNotExists,
PlaylistMaxSongsLimit,
UserPlaylistAddSong,
UserPlaylistNamesAutocomplete
} from '../../schemas/SchemaPlaylist.js';
import { Playlist } from 'distube';
import { generateErrorEmbed } from '../../utilities/generateErrorEmbed.js';
import { ENV } from '../../EnvironmentVariables.js';
import { loggerError } from '../../utilities/logger.js';
import { isValidURL } from '../../utilities/isValidURL.js';
import { generateSimpleEmbed } from '../../utilities/generateSimpleEmbed.js';

export default function (): ICommand {
return {
text_data: {
name: 'pl-add',
description: i18next.t('commands:pl-add_desc'),
arguments: [new CommandArgument(i18next.t('commands:pl-add_link'), true)],
execute: async (message: Message) => {}
arguments: [
new CommandArgument(i18next.t('commands:pl_arg_name'), true),
new CommandArgument(i18next.t('commands:pl_arg_song_url'), true)
],
execute: async (message: Message, args: Array<string>) => {
// With this we modify "args" to remove last argument and extract url
const url = args.pop() as string;
// Join all words for playlist name, when there is not url
const playlistName = args.join(' ');

await plAddAndReply(playlistName, url, message, message.author);
}
},
slash_data: {
slash_builder: new SlashCommandBuilder()
Expand All @@ -22,15 +43,103 @@ export default function (): ICommand {
.setDescription(i18next.t('commands:pl-add_link'))
.setAutocomplete(true)
.setRequired(true)
)
.addStringOption((option) =>
option.setName('song_url').setDescription(i18next.t('commands:pl_arg_song_url')).setRequired(true)
),
execute: async (interaction) => {},
execute: async (interaction) => {
const playlistName = interaction.options.getString('playlist_name')!;
const url = interaction.options.getString('song_url')!;

await plAddAndReply(playlistName, url, interaction, interaction.user);
},
autocomplete: UserPlaylistNamesAutocomplete
},
group: GroupAudio,
guild_data: {
guild_only: true,
voice_required: true
},
bot_permissions: [PermissionsBitField.Flags.SendMessages]
};
}

async function plAddAndReply(
playlistName: string,
url: string,
ctx: Message | ChatInputCommandInteraction,
user: User
) {
try {
if (!isValidURL(url)) {
await ctx.reply({
embeds: [generateErrorEmbed(i18next.t('commands:pl-add_error_song_must_be_link'))],
ephemeral: true
});
return;
}

const song = await ctx.client.audioPlayer.distube.handler.resolve(url);

if (song instanceof Playlist) {
await ctx.reply({
embeds: [generateErrorEmbed(i18next.t('commands:pl-add_error_song_must_not_be_playlist'))],
ephemeral: true
});
return;
}

if (song.isLive) {
await ctx.reply({
embeds: [generateErrorEmbed(i18next.t('commands:pl-add_error_song_must_not_be_live_stream'))],
ephemeral: true
});
return;
}

await UserPlaylistAddSong(user.id, playlistName, song);

await ctx.reply({
embeds: [
generateSimpleEmbed(
i18next.t('commands:pl-add_success', {
song: song.name,
playlist: playlistName,
interpolation: { escapeValue: false }
})
)
],
ephemeral: true
});
} catch (e) {
if (e instanceof PlaylistIsNotExists) {
await ctx.reply({
embeds: [
generateErrorEmbed(
i18next.t('commands:pl_error_playlist_not_exists', {
name: playlistName,
interpolation: { escapeValue: false }
})
)
],
ephemeral: true
});
return;
}

if (e instanceof PlaylistMaxSongsLimit) {
await ctx.reply({
embeds: [
generateErrorEmbed(
i18next.t('commands:pl-add_error_playlist_max_songs_limit', {
name: playlistName,
count: ENV.BOT_MAX_SONGS_IN_USER_PLAYLIST,
interpolation: { escapeValue: false }
})
)
],
ephemeral: true
});
return;
}

await ctx.reply({ embeds: [generateErrorEmbed(i18next.t('commands:pl-add_error_unknown'))], ephemeral: true });
if (ENV.BOT_VERBOSE_LOGGING) loggerError(e);
}
}
10 changes: 6 additions & 4 deletions src/commands/audio/pl-create.command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,15 @@ export default function (): ICommand {
};
}

async function plCreateAndReply(name: string, ctx: Message | ChatInputCommandInteraction, userID: string) {
async function plCreateAndReply(playlistName: string, ctx: Message | ChatInputCommandInteraction, userID: string) {
try {
await UserPlaylistCreate(userID, name);
await UserPlaylistCreate(userID, playlistName);

await ctx.reply({
embeds: [
generateSimpleEmbed(i18next.t('commands:pl-create_success', { name, interpolation: { escapeValue: false } }))
generateSimpleEmbed(
i18next.t('commands:pl-create_success', { name: playlistName, interpolation: { escapeValue: false } })
)
],
ephemeral: true
});
Expand All @@ -65,7 +67,7 @@ async function plCreateAndReply(name: string, ctx: Message | ChatInputCommandInt
embeds: [
generateErrorEmbed(
i18next.t('commands:pl-create_error_duplicate', {
name,
name: playlistName,
interpolation: { escapeValue: false }
})
)
Expand Down
1 change: 1 addition & 0 deletions src/commands/audio/pl-delete.command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ async function plDeleteAndReply(ctx: Message | ChatInputCommandInteraction, user
],
ephemeral: true
});
return;
}

await ctx.reply({ embeds: [generateErrorEmbed(i18next.t('commands:pl-delete_error'))], ephemeral: true });
Expand Down
53 changes: 50 additions & 3 deletions src/commands/audio/pl-display.command.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,33 @@
import { CommandArgument, ICommand } from '../../CommandTypes.js';
import { GroupAudio } from './AudioTypes.js';
import { Message, PermissionsBitField, SlashCommandBuilder } from 'discord.js';
import {
ChatInputCommandInteraction,
EmbedBuilder,
Message,
PermissionsBitField,
SlashCommandBuilder,
User
} from 'discord.js';
import i18next from 'i18next';
import {
PlaylistNameMaxLength,
PlaylistNameMinLength,
UserPlaylistGet,
UserPlaylistNamesAutocomplete
} from '../../schemas/SchemaPlaylist.js';
import { generateErrorEmbed } from '../../utilities/generateErrorEmbed.js';

export default function (): ICommand {
return {
text_data: {
name: 'pl-display',
description: i18next.t('commands:pl-display_desc'),
arguments: [new CommandArgument(i18next.t('commands:pl_arg_name'), true)],
execute: async (message: Message) => {}
execute: async (message: Message, args: Array<string>) => {
const playlistName = args[0];

await plDisplayAndReply(playlistName, message, message.author);
}
},
slash_data: {
slash_builder: new SlashCommandBuilder()
Expand All @@ -29,10 +42,44 @@ export default function (): ICommand {
.setMinLength(PlaylistNameMinLength)
.setMaxLength(PlaylistNameMaxLength)
),
execute: async (interaction) => {},
execute: async (interaction) => {
const playlistName = interaction.options.getString('playlist_name')!;

await plDisplayAndReply(playlistName, interaction, interaction.user);
},
autocomplete: UserPlaylistNamesAutocomplete
},
group: GroupAudio,
bot_permissions: [PermissionsBitField.Flags.SendMessages]
};
}

async function plDisplayAndReply(playlistName: string, ctx: Message | ChatInputCommandInteraction, user: User) {
const playlist = await UserPlaylistGet(user.id, playlistName, true);

if (!playlist) {
await ctx.reply({
embeds: [generateErrorEmbed(i18next.t('commands:pl_error_playlist_not_exists'))],
ephemeral: true
});
return;
}

const playlistEmbed = new EmbedBuilder().setAuthor({ name: user.displayName, iconURL: user.displayAvatarURL() });

let songs = ``;

playlist.songs.forEach((song, index) => {
const songDate = song.createdAt ? `<t:${Math.round(song.createdAt.getTime() / 1000)}:f>` : '<t:0:f>';

songs += `${index + 1}. [${song.name}](${song.url}) - ${songDate} \n`;
});

if (songs === '') {
playlistEmbed.setDescription(i18next.t('commands:pl-display_embed_no_songs'));
} else {
playlistEmbed.setDescription(songs);
}

await ctx.reply({ embeds: [playlistEmbed], ephemeral: true });
}
68 changes: 58 additions & 10 deletions src/commands/audio/pl-remove.command.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,30 @@
import { CommandArgument, ICommand } from '../../CommandTypes.js';
import { GroupAudio } from './AudioTypes.js';
import { Message, PermissionsBitField, SlashCommandBuilder } from 'discord.js';
import { ChatInputCommandInteraction, Message, PermissionsBitField, SlashCommandBuilder } from 'discord.js';
import i18next from 'i18next';
import { UserPlaylistNamesAutocomplete } from '../../schemas/SchemaPlaylist.js';
import { UserPlaylistNamesAutocomplete, UserPlaylistRemoveSong } from '../../schemas/SchemaPlaylist.js';
import { generateSimpleEmbed } from '../../utilities/generateSimpleEmbed.js';
import { generateErrorEmbed } from '../../utilities/generateErrorEmbed.js';
import { ENV } from '../../EnvironmentVariables.js';
import { loggerError } from '../../utilities/logger.js';

export default function (): ICommand {
return {
text_data: {
name: 'pl-remove',
description: i18next.t('commands:pl-remove_desc'),
arguments: [new CommandArgument(i18next.t('commands:pl-remove_link'), true)],
execute: async (message: Message) => {}
arguments: [
new CommandArgument(i18next.t('commands:pl_arg_name'), true),
new CommandArgument(i18next.t('commands:pl_arg_song_id'), true)
],
execute: async (message: Message, args: Array<string>) => {
// With this we modify "args" to extract songID and remain only words for playlist name
const songID = Number(args.pop());
// Join all words, when there is no song id
const playlistName = args.join(' ');

await plRemoveAndReply(playlistName, songID, message, message.author.id);
}
},
slash_data: {
slash_builder: new SlashCommandBuilder()
Expand All @@ -19,18 +33,52 @@ export default function (): ICommand {
.addStringOption((option) =>
option
.setName('playlist_name')
.setDescription(i18next.t('commands:pl-remove_link'))
.setDescription(i18next.t('commands:pl_arg_name'))
.setAutocomplete(true)
.setRequired(true)
)
.addNumberOption((option) =>
option.setName('song_id').setDescription(i18next.t('commands:pl_arg_song_id')).setRequired(true)
),
execute: async (interaction) => {},
execute: async (interaction) => {
const songID = interaction.options.getNumber('song_id')! - 1;
const playlistName = interaction.options.getString('playlist_name')!;

await plRemoveAndReply(playlistName, songID, interaction, interaction.user.id);
},
autocomplete: UserPlaylistNamesAutocomplete
},
group: GroupAudio,
guild_data: {
guild_only: true,
voice_required: true
},
bot_permissions: [PermissionsBitField.Flags.SendMessages]
};
}

async function plRemoveAndReply(
playlistName: string,
songID: number,
ctx: Message | ChatInputCommandInteraction,
userID: string
) {
try {
const playlistSong = await UserPlaylistRemoveSong(userID, playlistName, Number(songID));

await ctx.reply({
embeds: [
generateSimpleEmbed(
i18next.t('commands:pl-remove_success', {
song: playlistSong.name,
playlist: playlistName,
interpolation: { escapeValue: false }
})
)
],
ephemeral: true
});
} catch (e) {
await ctx.reply({
embeds: [generateErrorEmbed(i18next.t('commands:pl-remove_error_unknown'))],
ephemeral: true
});
if (ENV.BOT_VERBOSE_LOGGING) loggerError(e);
}
}
Loading

0 comments on commit 464f488

Please sign in to comment.