diff --git a/README.md b/README.md index 2fe1e28..6524aa5 100644 --- a/README.md +++ b/README.md @@ -27,3 +27,13 @@ Feedr strives for constant improvement, so here's what will be implemented * Embed Links * Attach Files * Add Reactions + +# Changelog +## 1.1.0 +* Replies are no longer deferred +* Messages can now be sent in Announcement channels [1.0.3] +* Better checking for valid YouTube channel IDs +* Channels with no uploads will be tracked now + +## 1.0.0 +* Initial release \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index d0bf25c..cd1ebe2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "videonotifier", - "version": "1.0.0", + "version": "1.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "videonotifier", - "version": "1.0.0", + "version": "1.1.0", "dependencies": { "discord-api-types": "^0.37.93", "discord.js": "^14.15.3", diff --git a/package.json b/package.json index faa9e4b..6151a77 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "videonotifier", "module": "src/index.ts", "type": "module", - "version": "1.0.0", + "version": "1.1.0", "devDependencies": { "@types/bun": "1.1.6" }, diff --git a/src/commands.ts b/src/commands.ts index e056bd5..0af7120 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -148,10 +148,14 @@ const commands: Record = { const discordChannelId = interaction.options.get('updates_channel')?.value as string; const guildId = interaction.guildId; - // Deferring the reply is not the best practice, - // but in case the network/database is slow, it's better to defer the reply - // so we don't get a timeout error - await interaction.deferReply(); + // Check that the channel ID is in a valid format + if (youtubeChannelId.length != 24 || !youtubeChannelId.startsWith('UC')) { + await interaction.reply({ + ephemeral: true, + content: 'Invalid YouTube channel ID format!', + }); + return; + } // DMs are currently not supported, so throw back an error if (!guildId || interaction.channel?.isDMBased()) { @@ -173,24 +177,28 @@ const commands: Record = { // Check if the bot has the required permissions for the target channel const targetChannel = await client.channels.fetch(discordChannelId); - if (targetChannel && targetChannel.type === ChannelType.GuildText) { + if (targetChannel && (targetChannel.type === ChannelType.GuildText || targetChannel.type === ChannelType.GuildAnnouncement)) { + const requiredPermissions = [ + { flag: PermissionFlagsBits.ViewChannel, name: 'View Channel' }, + { flag: PermissionFlagsBits.SendMessages, name: 'Send Messages' }, + { flag: PermissionFlagsBits.SendMessagesInThreads, name: 'Send Messages in Threads' }, + { flag: PermissionFlagsBits.EmbedLinks, name: 'Embed Links' }, + { flag: PermissionFlagsBits.AttachFiles, name: 'Attach Files' }, + { flag: PermissionFlagsBits.AddReactions, name: 'Add Reactions' } + ]; const botPermissions = targetChannel.permissionsFor(client.user?.id as unknown as GuildMember); - if ( - !botPermissions?.has(PermissionFlagsBits.ViewChannel) || - !botPermissions?.has(PermissionFlagsBits.SendMessages) || - !botPermissions?.has(PermissionFlagsBits.SendMessagesInThreads) || - !botPermissions?.has(PermissionFlagsBits.EmbedLinks) || - !botPermissions?.has(PermissionFlagsBits.AttachFiles) || - !botPermissions?.has(PermissionFlagsBits.AddReactions) - ) { - await interaction.followUp({ + const missingPermissions = requiredPermissions + .filter(permission => !botPermissions?.has(permission.flag)) + .map(permission => permission.name); + if (missingPermissions.length > 0) { + await interaction.reply({ ephemeral: true, - content: 'The bot does not have the required permissions for the target channel!', + content: `The bot does not have the required permissions for the target channel! Missing permissions: ${missingPermissions.join(', ')}`, }); return; } } else { - await interaction.followUp({ + await interaction.reply({ ephemeral: true, content: 'The target channel is not a text channel!', }); @@ -199,16 +207,16 @@ const commands: Record = { // Check if the channel is valid if (!await checkIfChannelIdIsValid(youtubeChannelId)) { - await interaction.followUp({ + await interaction.reply({ ephemeral: true, - content: 'Invalid YouTube channel ID!', + content: 'That channel doesn\'t exist!', }); return; } // Check if the channel is already being tracked in the guild if (await checkIfGuildIsTrackingChannelAlready(youtubeChannelId, guildId)) { - await interaction.followUp({ + await interaction.reply({ ephemeral: true, content: 'This channel is already being tracked!', }); @@ -218,7 +226,7 @@ const commands: Record = { // Check if the channel is already being tracked globally if (!await checkIfChannelIsAlreadyTracked(youtubeChannelId)) { if (!await addNewChannelToTrack(youtubeChannelId)) { - await interaction.followUp({ + await interaction.reply({ ephemeral: true, content: 'An error occurred while trying to add the channel to track! This is a new channel being tracked globally, please report this error!', }); @@ -229,15 +237,15 @@ const commands: Record = { // Add the guild to the database if (await addNewGuildToTrackChannel(guildId, youtubeChannelId, discordChannelId, interaction.options.get('role')?.value as string ?? null)) { const channelIdInfo = await client.channels.fetch(discordChannelId); - if (channelIdInfo && channelIdInfo.type === ChannelType.GuildText) { + if (channelIdInfo && (channelIdInfo.type === ChannelType.GuildText || channelIdInfo.type === ChannelType.GuildAnnouncement)) { const youtubeChannelInfo = await getChannelDetails(youtubeChannelId) - await interaction.followUp({ + await interaction.reply({ ephemeral: true, content: `Started tracking the channel ${youtubeChannelInfo?.channelName ?? youtubeChannelId} in ${channelIdInfo.name}!`, }); } else { - await interaction.followUp({ + await interaction.reply({ ephemeral: true, content: 'The channel to send updates to is not a text channel! Please make sure to set a text channel!', }); diff --git a/src/database.ts b/src/database.ts index 608060e..d0203fa 100644 --- a/src/database.ts +++ b/src/database.ts @@ -14,7 +14,7 @@ export async function initTables(): Promise { const createYouTubeTable = ` CREATE TABLE IF NOT EXISTS youtube ( youtube_channel_id VARCHAR(255) NOT NULL PRIMARY KEY, - latest_video_id VARCHAR(255) NOT NULL UNIQUE + latest_video_id VARCHAR(255) UNIQUE ); `; const createDiscordTable = ` @@ -71,7 +71,7 @@ export async function addNewChannelToTrack(channelId: string) { } const data = await res.json(); - const videoId = data.items[0].snippet.thumbnails.default.url.split('/')[4]; + const videoId = data.items?.[0]?.snippet?.thumbnails?.default?.url?.split('/')[4] || null; const query = `INSERT INTO youtube (youtube_channel_id, latest_video_id) VALUES (?, ?)`; return new Promise((resolve, reject) => diff --git a/src/utils/checkIfChannelIdIsValid.ts b/src/utils/checkIfChannelIdIsValid.ts index 272780f..bf808a8 100644 --- a/src/utils/checkIfChannelIdIsValid.ts +++ b/src/utils/checkIfChannelIdIsValid.ts @@ -1,6 +1,7 @@ import { env } from "../config" export default async function checkIfChannelIdIsValid(channelId: string) { - const res = await fetch(`https://www.googleapis.com/youtube/v3/channels?part=snippet&id=${channelId}&key=${env.youtubeApiKey}`) - return res.ok + const res = await fetch(`https://www.googleapis.com/youtube/v3/channels?part=snippet&id=${channelId}&key=${env.youtubeApiKey}`); + const data = await res.json(); + return data.items !== undefined && data.items.length > 0; } \ No newline at end of file diff --git a/src/utils/fetchLatestUploads.ts b/src/utils/fetchLatestUploads.ts index 8dc18ca..a490a9e 100644 --- a/src/utils/fetchLatestUploads.ts +++ b/src/utils/fetchLatestUploads.ts @@ -63,10 +63,10 @@ export default async function fetchLatestUploads() { } await (channelObj as TextChannel).send({ - content: guild.guild_ping_role && channelInfo ? `<@&${guild.guild_ping_role}> New video uploaded for ${channelInfo?.channelName}! https://www.youtube.com/watch?v=${videoId}` : + content: guild.guild_ping_role && channelInfo ? `<@&${guild.guild_ping_role}> New video uploaded for ${channelInfo?.channelName}! https://www.youtube.com/watch?v=${videoId}` : guild.guild_ping_role ? `<@&${guild.guild_ping_role}> New video uploaded! https://www.youtube.com/watch?v=${videoId}` : - channelInfo ? `New video uploaded for ${channelInfo.channelName}! https://www.youtube.com/watch?v=${videoId}` : - `New video uploaded! https://www.youtube.com/watch?v=${videoId}` + channelInfo ? `New video uploaded for ${channelInfo.channelName}! https://www.youtube.com/watch?v=${videoId}` : + `New video uploaded! https://www.youtube.com/watch?v=${videoId}` }); } catch (error) { console.error("Error fetching or sending message to channel in fetchLatestUploads:", error);