Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/bot.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import {Client, Collection, User} from 'discord.js';
import {Client, Collection, User, GuildMember, PartialGuildMember} from 'discord.js';
import {inject, injectable} from 'inversify';
import ora from 'ora';
import {TYPES} from './types.js';
import container from './inversify.config.js';
import Command from './commands/index.js';
import debug from './utils/debug.js';
import handleGuildCreate from './events/guild-create.js';
import handleGuildMemberAdd from './events/guild-member-add.js';

Check failure on line 9 in src/bot.ts

View workflow job for this annotation

GitHub Actions / Type Check

Cannot find module './events/guild-member-add.js' or its corresponding type declarations.
import handleGuildMemberRemove from './events/guild-member-remove.js';
import handleVoiceStateUpdate from './events/voice-state-update.js';
import errorMsg from './utils/error-msg.js';
import {isUserInVoice} from './utils/channels.js';
Expand Down Expand Up @@ -162,6 +164,8 @@
this.client.on('debug', debug);

this.client.on('guildCreate', handleGuildCreate);
this.client.on('guildMemberAdd', handleGuildMemberAdd);
this.client.on('guildMemberRemove', (member: GuildMember | PartialGuildMember) => handleGuildMemberRemove(member));

Check failure on line 168 in src/bot.ts

View workflow job for this annotation

GitHub Actions / Lint

Functions that return promises must be async
this.client.on('voiceStateUpdate', handleVoiceStateUpdate);
await this.client.login();
}
Expand Down
144 changes: 142 additions & 2 deletions src/commands/config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {SlashCommandBuilder} from '@discordjs/builders';
import {ChatInputCommandInteraction, EmbedBuilder, PermissionFlagsBits} from 'discord.js';
import {ChannelType, ChatInputCommandInteraction, EmbedBuilder, PermissionFlagsBits} from 'discord.js';
import {injectable} from 'inversify';
import {prisma} from '../utils/db.js';
import Command from './index.js';
Expand Down Expand Up @@ -81,11 +81,49 @@
.setMinValue(1)
.setMaxValue(30)
.setRequired(true)))
.addSubcommand(subcommand => subcommand
.setName('set-welcome-channel')
.setDescription('set where welcome messages are posted')
.addChannelOption(option => option
.setName('channel')
.setDescription('welcome channel')
.addChannelTypes([ChannelType.GuildText])

Check failure on line 90 in src/commands/config.ts

View workflow job for this annotation

GitHub Actions / Type Check

Argument of type 'ChannelType[]' is not assignable to parameter of type 'ChannelType.GuildText | ChannelType.GuildVoice | ChannelType.GuildCategory | ChannelType.GuildNews | ChannelType.GuildNewsThread | ChannelType.GuildPublicThread | ChannelType.GuildPrivateThread | ChannelType.GuildStageVoice'.
.setRequired(true)))
.addSubcommand(subcommand => subcommand
.setName('clear-welcome-channel')
.setDescription('turn off welcome messages'))
.addSubcommand(subcommand => subcommand
.setName('set-welcome-message')
.setDescription('set the welcome message. Supports {user}, {username}, {server}, {memberCount}')
.addStringOption(option => option
.setName('message')
.setDescription('message template')
.setMaxLength(500)
.setRequired(true)))
.addSubcommand(subcommand => subcommand
.setName('set-leave-channel')
.setDescription('set where leave messages are posted')
.addChannelOption(option => option
.setName('channel')
.setDescription('leave channel')
.addChannelTypes([ChannelType.GuildText])

Check failure on line 109 in src/commands/config.ts

View workflow job for this annotation

GitHub Actions / Type Check

Argument of type 'ChannelType[]' is not assignable to parameter of type 'ChannelType.GuildText | ChannelType.GuildVoice | ChannelType.GuildCategory | ChannelType.GuildNews | ChannelType.GuildNewsThread | ChannelType.GuildPublicThread | ChannelType.GuildPrivateThread | ChannelType.GuildStageVoice'.
.setRequired(true)))
.addSubcommand(subcommand => subcommand
.setName('clear-leave-channel')
.setDescription('turn off leave messages'))
.addSubcommand(subcommand => subcommand
.setName('set-leave-message')
.setDescription('set the leave message. Supports {user}, {username}, {server}, {memberCount}')
.addStringOption(option => option
.setName('message')
.setDescription('message template')
.setMaxLength(500)
.setRequired(true)))
.addSubcommand(subcommand => subcommand
.setName('get')
.setDescription('show all settings'));

async execute(interaction: ChatInputCommandInteraction) {

Check warning on line 126 in src/commands/config.ts

View workflow job for this annotation

GitHub Actions / Lint

Async method 'execute' has a complexity of 26. Maximum allowed is 20
// Ensure guild settings exist before trying to update
await getGuildSettings(interaction.guild!.id);

Expand Down Expand Up @@ -213,6 +251,104 @@
break;
}

case 'set-welcome-channel': {
const channel = interaction.options.getChannel('channel', true);

await prisma.setting.update({
where: {
guildId: interaction.guild!.id,
},
data: {
welcomeChannelId: channel.id,

Check failure on line 262 in src/commands/config.ts

View workflow job for this annotation

GitHub Actions / Type Check

Type '{ welcomeChannelId: string; }' is not assignable to type '(Without<SettingUpdateInput, SettingUncheckedUpdateInput> & SettingUncheckedUpdateInput) | (Without<...> & SettingUpdateInput)'.
},
});

await interaction.reply(`welcome messages will go to <#${channel.id}>`);

break;
}

case 'clear-welcome-channel': {
await prisma.setting.update({
where: {
guildId: interaction.guild!.id,
},
data: {
welcomeChannelId: null,

Check failure on line 277 in src/commands/config.ts

View workflow job for this annotation

GitHub Actions / Type Check

Type '{ welcomeChannelId: null; }' is not assignable to type '(Without<SettingUpdateInput, SettingUncheckedUpdateInput> & SettingUncheckedUpdateInput) | (Without<...> & SettingUpdateInput)'.
},
});

await interaction.reply('welcome messages disabled');

break;
}

case 'set-welcome-message': {
const message = interaction.options.getString('message', true);

await prisma.setting.update({
where: {
guildId: interaction.guild!.id,
},
data: {
welcomeMessage: message,

Check failure on line 294 in src/commands/config.ts

View workflow job for this annotation

GitHub Actions / Type Check

Type '{ welcomeMessage: string; }' is not assignable to type '(Without<SettingUpdateInput, SettingUncheckedUpdateInput> & SettingUncheckedUpdateInput) | (Without<...> & SettingUpdateInput)'.
},
});

await interaction.reply('welcome message updated');

break;
}

case 'set-leave-channel': {
const channel = interaction.options.getChannel('channel', true);

await prisma.setting.update({
where: {
guildId: interaction.guild!.id,
},
data: {
leaveChannelId: channel.id,

Check failure on line 311 in src/commands/config.ts

View workflow job for this annotation

GitHub Actions / Type Check

Type '{ leaveChannelId: string; }' is not assignable to type '(Without<SettingUpdateInput, SettingUncheckedUpdateInput> & SettingUncheckedUpdateInput) | (Without<...> & SettingUpdateInput)'.
},
});

await interaction.reply(`leave messages will go to <#${channel.id}>`);

break;
}

case 'clear-leave-channel': {
await prisma.setting.update({
where: {
guildId: interaction.guild!.id,
},
data: {
leaveChannelId: null,

Check failure on line 326 in src/commands/config.ts

View workflow job for this annotation

GitHub Actions / Type Check

Type '{ leaveChannelId: null; }' is not assignable to type '(Without<SettingUpdateInput, SettingUncheckedUpdateInput> & SettingUncheckedUpdateInput) | (Without<...> & SettingUpdateInput)'.
},
});

await interaction.reply('leave messages disabled');

break;
}

case 'set-leave-message': {
const message = interaction.options.getString('message', true);

await prisma.setting.update({
where: {
guildId: interaction.guild!.id,
},
data: {
leaveMessage: message,

Check failure on line 343 in src/commands/config.ts

View workflow job for this annotation

GitHub Actions / Type Check

Type '{ leaveMessage: string; }' is not assignable to type '(Without<SettingUpdateInput, SettingUncheckedUpdateInput> & SettingUncheckedUpdateInput) | (Without<...> & SettingUpdateInput)'.
},
});

await interaction.reply('leave message updated');

break;
}

case 'set-reduce-vol-when-voice': {
const value = interaction.options.getBoolean('value')!;

Expand Down Expand Up @@ -259,15 +395,19 @@
: `${config.secondsToWaitAfterQueueEmpties}s`,
'Leave if there are no listeners': config.leaveIfNoListeners ? 'yes' : 'no',
'Auto announce next song in queue': config.autoAnnounceNextSong ? 'yes' : 'no',
'Add to queue reponses show for requester only': config.autoAnnounceNextSong ? 'yes' : 'no',
'Add to queue responses show for requester only': config.queueAddResponseEphemeral ? 'yes' : 'no',
'Default Volume': config.defaultVolume,
'Default queue page size': config.defaultQueuePageSize,
'Reduce volume when people speak': config.turnDownVolumeWhenPeopleSpeak ? 'yes' : 'no',
'Welcome channel': config.welcomeChannelId ? `<#${config.welcomeChannelId}>` : 'disabled',

Check failure on line 402 in src/commands/config.ts

View workflow job for this annotation

GitHub Actions / Type Check

Property 'welcomeChannelId' does not exist on type 'GetResult<{ guildId: string; playlistLimit: number; secondsToWaitAfterQueueEmpties: number; leaveIfNoListeners: boolean; queueAddResponseEphemeral: boolean; autoAnnounceNextSong: boolean; ... 5 more ...; updatedAt: Date; }, unknown, never> & {}'.

Check failure on line 402 in src/commands/config.ts

View workflow job for this annotation

GitHub Actions / Lint

Invalid type "any" of template literal expression
'Welcome message': config.welcomeMessage,

Check failure on line 403 in src/commands/config.ts

View workflow job for this annotation

GitHub Actions / Lint

Unsafe assignment of an `any` value
'Leave channel': config.leaveChannelId ? `<#${config.leaveChannelId}>` : 'disabled',

Check failure on line 404 in src/commands/config.ts

View workflow job for this annotation

GitHub Actions / Lint

Invalid type "any" of template literal expression
'Leave message': config.leaveMessage,

Check failure on line 405 in src/commands/config.ts

View workflow job for this annotation

GitHub Actions / Lint

Unsafe assignment of an `any` value
};

let description = '';
for (const [key, value] of Object.entries(settingsToShow)) {
description += `**${key}**: ${value}\n`;

Check failure on line 410 in src/commands/config.ts

View workflow job for this annotation

GitHub Actions / Lint

Invalid type "any" of template literal expression
}

embed.setDescription(description);
Expand Down
10 changes: 10 additions & 0 deletions src/events/guild-member-remove.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import {GuildMember, PartialGuildMember} from 'discord.js';
import {getGuildSettings} from '../utils/get-guild-settings.js';
import {formatMemberMessage, sendMemberMessage} from '../utils/member-messages.js';

export default async function handleGuildMemberRemove(member: GuildMember | PartialGuildMember): Promise<void> {
const settings = await getGuildSettings(member.guild.id);
const message = formatMemberMessage(settings.leaveMessage, member);

Check failure on line 7 in src/events/guild-member-remove.ts

View workflow job for this annotation

GitHub Actions / Lint

Unsafe call of an `any` typed value

Check failure on line 7 in src/events/guild-member-remove.ts

View workflow job for this annotation

GitHub Actions / Lint

Unsafe assignment of an `any` value

await sendMemberMessage(settings.leaveChannelId, member, {content: message});

Check failure on line 9 in src/events/guild-member-remove.ts

View workflow job for this annotation

GitHub Actions / Lint

Unsafe assignment of an `any` value

Check failure on line 9 in src/events/guild-member-remove.ts

View workflow job for this annotation

GitHub Actions / Lint

Unsafe call of an `any` typed value
}
71 changes: 66 additions & 5 deletions src/utils/build-embed.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
import getYouTubeID from 'get-youtube-id';
import {EmbedBuilder} from 'discord.js';
import {ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder, APIActionRowComponent, APIMessageActionRowComponent} from 'discord.js';
import Player, {MediaSource, QueuedSong, STATUS} from '../services/player.js';
import getProgressBar from './get-progress-bar.js';
import {prettyTime} from './time.js';
import {truncate} from './string.js';

export const MUSIC_BUTTON_IDS = {
replay: 'music:replay',
pauseResume: 'music:pause-resume',
skip: 'music:skip',
stop: 'music:stop',
loopSong: 'music:loop-song',
loopQueue: 'music:loop-queue',
shuffle: 'music:shuffle',
queue: 'music:queue',
} as const;

const getMaxSongTitleLength = (title: string) => {
// eslint-disable-next-line no-control-regex
const nonASCII = /[^\x00-\x7F]+/;
Expand Down Expand Up @@ -33,6 +44,18 @@ const getQueueInfo = (player: Player) => {
return queueSize === 1 ? '1 song' : `${queueSize} songs`;
};

const getLoopLabel = (player: Player) => {
if (player.loopCurrentSong) {
return 'Song';
}

if (player.loopCurrentQueue) {
return 'Queue';
}

return 'Off';
};

const getPlayerUI = (player: Player) => {
const song = player.getCurrent();

Expand Down Expand Up @@ -75,6 +98,45 @@ export const buildPlayingMessageEmbed = (player: Player): EmbedBuilder => {
return message;
};

export const buildPlayerControlRows = (player: Player): Array<APIActionRowComponent<APIMessageActionRowComponent>> => [
new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder()
.setCustomId(MUSIC_BUTTON_IDS.replay)
.setLabel('Restart')
.setStyle(ButtonStyle.Secondary),
new ButtonBuilder()
.setCustomId(MUSIC_BUTTON_IDS.pauseResume)
.setLabel(player.status === STATUS.PLAYING ? 'Pause' : 'Resume')
.setStyle(player.status === STATUS.PLAYING ? ButtonStyle.Primary : ButtonStyle.Success),
new ButtonBuilder()
.setCustomId(MUSIC_BUTTON_IDS.skip)
.setLabel('Skip')
.setStyle(ButtonStyle.Primary),
new ButtonBuilder()
.setCustomId(MUSIC_BUTTON_IDS.stop)
.setLabel('Stop')
.setStyle(ButtonStyle.Danger),
).toJSON() as APIActionRowComponent<APIMessageActionRowComponent>,
new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder()
.setCustomId(MUSIC_BUTTON_IDS.loopSong)
.setLabel(`Repeat Song: ${player.loopCurrentSong ? 'On' : 'Off'}`)
.setStyle(player.loopCurrentSong ? ButtonStyle.Success : ButtonStyle.Secondary),
new ButtonBuilder()
.setCustomId(MUSIC_BUTTON_IDS.loopQueue)
.setLabel(`Repeat Queue: ${player.loopCurrentQueue ? 'On' : 'Off'}`)
.setStyle(player.loopCurrentQueue ? ButtonStyle.Success : ButtonStyle.Secondary),
new ButtonBuilder()
.setCustomId(MUSIC_BUTTON_IDS.shuffle)
.setLabel('Shuffle')
.setStyle(ButtonStyle.Secondary),
new ButtonBuilder()
.setCustomId(MUSIC_BUTTON_IDS.queue)
.setLabel('Queue')
.setStyle(ButtonStyle.Secondary),
).toJSON() as APIActionRowComponent<APIMessageActionRowComponent>,
];

export const buildQueueEmbed = (player: Player, page: number, pageSize: number): EmbedBuilder => {
const currentlyPlaying = player.getCurrent();

Expand Down Expand Up @@ -118,12 +180,12 @@ export const buildQueueEmbed = (player: Player, page: number, pageSize: number):
}

message
.setTitle(player.status === STATUS.PLAYING ? `Now Playing ${player.loopCurrentSong ? '(loop on)' : ''}` : 'Queued songs')
.setColor(player.status === STATUS.PLAYING ? 'DarkGreen' : 'NotQuiteBlack')
.setTitle(player.status === STATUS.PLAYING ? 'Now Playing' : 'Queued Songs')
.setColor(player.status === STATUS.PLAYING ? 0x00E5A8 : 0x5865F2)
.setDescription(description)
.addFields([{name: 'In queue', value: getQueueInfo(player), inline: true}, {
name: 'Total length', value: `${totalLength > 0 ? prettyTime(totalLength) : '-'}`, inline: true,
}, {name: 'Page', value: `${page} out of ${maxQueuePage}`, inline: true}])
}, {name: 'Page', value: `${page} out of ${maxQueuePage}`, inline: true}, {name: 'Repeat', value: getLoopLabel(player), inline: true}])
.setFooter({text: `Source: ${artist} ${playlistTitle}`});

if (thumbnailUrl) {
Expand All @@ -132,4 +194,3 @@ export const buildQueueEmbed = (player: Player, page: number, pageSize: number):

return message;
};

Loading