From f428f134bf6ed312e255aa3708e1339819382a9a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 14 Dec 2024 10:41:09 +0000 Subject: [PATCH 1/7] build(deps-dev): bump @types/bun from 1.1.10 to 1.1.14 Bumps [@types/bun](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/bun) from 1.1.10 to 1.1.14. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/bun) --- updated-dependencies: - dependency-name: "@types/bun" dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 571414e..bda36ba 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "type": "module", "version": "1.3.0", "devDependencies": { - "@types/bun": "1.1.10", + "@types/bun": "1.1.14", "@typescript-eslint/eslint-plugin": "8.11.0", "@typescript-eslint/parser": "8.11.0", "eslint": "^8.57.0", From 597e6163e991cf531c9d031ebee45124cc545d69 Mon Sep 17 00:00:00 2001 From: GalvinPython <77013913+GalvinPython@users.noreply.github.com> Date: Sat, 14 Dec 2024 10:45:52 +0000 Subject: [PATCH 2/7] chore(workflows): allow lockb.yml workflow to run on dev branch --- .github/workflows/lockb.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/lockb.yml b/.github/workflows/lockb.yml index 96ddd3c..cc54e8d 100644 --- a/.github/workflows/lockb.yml +++ b/.github/workflows/lockb.yml @@ -2,8 +2,7 @@ name: Update bun.lockb on: push: - branches: - - main + branches: [main, dev] permissions: contents: write From 4da0fad80d830141e602783578a017a7c05b9f74 Mon Sep 17 00:00:00 2001 From: GalvinPython <77013913+GalvinPython@users.noreply.github.com> Date: Sat, 14 Dec 2024 10:48:05 +0000 Subject: [PATCH 3/7] chore(workflows): never hard code your workflow branches --- .github/workflows/lockb.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lockb.yml b/.github/workflows/lockb.yml index cc54e8d..dd47c7d 100644 --- a/.github/workflows/lockb.yml +++ b/.github/workflows/lockb.yml @@ -39,5 +39,5 @@ jobs: git config --global user.name 'github-actions[bot]' git config --global user.email 'github-actions[bot]@users.noreply.github.com' git commit -m "Update bun.lockb (via GitHub Actions)" - git pull --rebase origin main - git push + git pull --rebase origin ${{ github.ref_name }} + git push origin ${{ github.ref_name }} From fb6e6ba5f24d7224eed6ca231845dde9d37f435b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 14 Dec 2024 10:48:22 +0000 Subject: [PATCH 4/7] Update bun.lockb (via GitHub Actions) --- bun.lockb | Bin 108430 -> 108430 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/bun.lockb b/bun.lockb index 5409baa3fb38dc739960b230d60436582d69c5f4..0c71cd596b9762941f2b41ba986bcc05b46fd23e 100755 GIT binary patch delta 293 zcmeA>&(?RIZGxVHyu`8_X~y#C zEZpl-_b+c;UvyqIG4d9-!KsZiKW8S-sh6J`XmVeC`jHh$Hz%$YQ{Exfb;QeO<)f`z zR&5oK$yz4lzC3w*jH2?oh8)Jt6OYC-ZeDOSfN}DIqvk-jvuv(7`Y?piWP6kpV~hop pvHA8LL5yF6IZX6Q(yNk6^QPOzGAd5Lwv17V&Db0yHhJRF82|@Ddqw~N delta 293 zcmeA>&(?RIZGxVHUsZs+>)JY==h6*AdG zOk?r_L5|5QLIfCPCnpMOZ+;RYm|OpT(Tq?XZF#Q0=T1I()pXq=+g9sOV{@s=>xJFc zT~A(2OS!J)Vv{h1cV3Z|qR@%PYn!hdepA@9-ZyV+bNS8kv<5~721bT|{QrP7FA&23 zi%{)a7B0l`!0=Mn9`Lm=v_1Tz~dcNtl zpIs;Utb_Z9!}5-8COgcU>=`#tJQ~lqdBM>D#>oqgngiX=vbo~u!w^P;?NL&UF&0cl pmfLp(F@6o^FwiSWuSzP-n{FG+s5t%FGDayjBTJCj Date: Sat, 14 Dec 2024 13:48:36 +0000 Subject: [PATCH 5/7] fix(bot): format status (closes #43) --- src/events/ready.ts | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/events/ready.ts b/src/events/ready.ts index 88674a9..9fe5a61 100644 --- a/src/events/ready.ts +++ b/src/events/ready.ts @@ -1,28 +1,31 @@ -import { ActivityType, Events, PresenceUpdateStatus } from 'discord.js'; -import client from '../index'; -import fetchLatestUploads from '../utils/youtube/fetchLatestUploads'; -import { config } from '../config'; -import { checkIfStreamersAreLive } from '../utils/twitch/checkIfStreamerIsLive'; -import { updateBotInfo } from '../utils/database'; +import { ActivityType, Events, PresenceUpdateStatus } from "discord.js"; + +import client from "../index"; +import fetchLatestUploads from "../utils/youtube/fetchLatestUploads"; +import { config } from "../config"; +import { checkIfStreamersAreLive } from "../utils/twitch/checkIfStreamerIsLive"; +import { updateBotInfo } from "../utils/database"; // update the bot's presence async function updatePresence() { if (!client?.user) return; const servers = client.guilds.cache.size; - const members = client.guilds.cache.reduce((acc, guild) => acc + guild.memberCount, 0) + const members = client.guilds.cache.reduce( + (acc, guild) => acc + guild.memberCount, + 0, + ); await updateBotInfo(servers, members); client.user.setPresence({ activities: [ { - name: `Notifying ${servers} servers [${members} members]`, + name: `Notifying ${servers.toLocaleString()} servers [${members.toLocaleString()} members]`, type: ActivityType.Custom, }, ], status: PresenceUpdateStatus.Online, }); - } // Log into the bot From cf661ba3c990c9efda1b8dc8b6fc0361794eb14e Mon Sep 17 00:00:00 2001 From: GalvinPython <77013913+GalvinPython@users.noreply.github.com> Date: Sun, 15 Dec 2024 22:18:32 +0000 Subject: [PATCH 6/7] feat(bot): add innertube search --- src/types/innertube.d.ts | 176 ++++++++++++++++++++++++++++++++++++ src/utils/youtube/search.ts | 32 +++++++ 2 files changed, 208 insertions(+) create mode 100644 src/types/innertube.d.ts create mode 100644 src/utils/youtube/search.ts diff --git a/src/types/innertube.d.ts b/src/types/innertube.d.ts new file mode 100644 index 0000000..4b51f95 --- /dev/null +++ b/src/types/innertube.d.ts @@ -0,0 +1,176 @@ +// NOTE: Experimental +// You think i was typing this all out manually? lol no :p + +export type InnertubeSearchRequest = { + contents: { + twoColumnSearchResultsRenderer: { + primaryContents: { + sectionListRenderer: { + contents: Array<{ + itemSectionRenderer?: { + contents: Array<{ + didYouMeanRenderer?: { + didYouMean: { + runs: Array<{ + text: string; + }>; + }; + correctedQuery: { + runs: Array<{ + text: string; + italics: boolean; + }>; + }; + correctedQueryEndpoint: { + clickTrackingParams: string; + commandMetadata: { + webCommandMetadata: { + url: string; + webPageType: string; + rootVe: number; + }; + }; + searchEndpoint: { + query: string; + params: string; + }; + }; + trackingParams: string; + }; + channelRenderer?: { + channelId: string; + title: { + simpleText: string; + }; + navigationEndpoint: { + clickTrackingParams: string; + commandMetadata: { + webCommandMetadata: { + url: string; + webPageType: string; + rootVe: number; + apiUrl: string; + }; + }; + browseEndpoint: { + browseId: string; + canonicalBaseUrl: string; + }; + }; + thumbnail: { + thumbnails: Array<{ + url: string; + width: number; + height: number; + }>; + }; + descriptionSnippet?: { + runs: Array<{ + text: string; + bold?: boolean; + }>; + }; + shortBylineText: { + runs: Array<{ + text: string; + navigationEndpoint: { + clickTrackingParams: string; + commandMetadata: { + webCommandMetadata: { + url: string; + webPageType: string; + rootVe: number; + apiUrl: string; + }; + }; + browseEndpoint: { + browseId: string; + canonicalBaseUrl: string; + }; + }; + }>; + }; + videoCountText: { + accessibility: { + accessibilityData: { + label: string; + }; + }; + simpleText: string; + }; + subscriptionButton: { + subscribed: boolean; + }; + subscriberCountText: { + simpleText: string; + }; + subscribeButton: { + buttonRenderer: { + style: string; + size: string; + isDisabled: boolean; + text: { + runs: Array<{ + text: string; + }>; + }; + navigationEndpoint: { + clickTrackingParams: string; + commandMetadata: { + webCommandMetadata: { + url: string; + webPageType: string; + rootVe: number; + }; + }; + signInEndpoint: { + nextEndpoint: { + clickTrackingParams: string; + commandMetadata: { + webCommandMetadata: { + url: string; + webPageType: string; + rootVe: number; + }; + }; + searchEndpoint: { + query: string; + params: string; + }; + }; + continueAction: string; + }; + }; + trackingParams: string; + }; + }; + trackingParams: string; + longBylineText: { + runs: Array<{ + text: string; + navigationEndpoint: { + clickTrackingParams: string; + commandMetadata: { + webCommandMetadata: { + url: string; + webPageType: string; + rootVe: number; + apiUrl: string; + }; + }; + browseEndpoint: { + browseId: string; + canonicalBaseUrl: string; + }; + }; + }>; + }; + }; + }>; + }; + }>; + }; + }; + }; + }; +}; diff --git a/src/utils/youtube/search.ts b/src/utils/youtube/search.ts new file mode 100644 index 0000000..08db97e --- /dev/null +++ b/src/utils/youtube/search.ts @@ -0,0 +1,32 @@ +// NOTE: Experimental +import type { InnertubeSearchRequest } from "../../types/innertube"; + +export default async function (query: string) { + try { + const response = await fetch( + "https://www.youtube.com/youtubei/v1/search?prettyPrint=false", + { + headers: { + "X-Goog-Fieldmask": + "contents.twoColumnSearchResultsRenderer.primaryContents.sectionListRenderer.contents.itemSectionRenderer.contents", + }, + body: JSON.stringify({ + context: { + client: { + clientName: "WEB", + clientVersion: "2.20241212.08.00", + }, + }, + params: "EgIQAg%3D%3D", + query: query, + }), + method: "POST", + }, + ); + const data = (await response.json()) as Promise; + + console.dir(data, { depth: null }); + } catch (err) { + console.error(err); + } +} From 9715bd311be44b10e666b81156a50fae8a95823e Mon Sep 17 00:00:00 2001 From: GalvinPython <77013913+GalvinPython@users.noreply.github.com> Date: Sun, 29 Dec 2024 20:10:09 +0000 Subject: [PATCH 7/7] feat(bot): added /tracked command --- README.md | 9 +- src/commands.ts | 478 +++++++++++++++++++++++++++++----------- src/types/database.d.ts | 8 + src/utils/database.ts | 21 +- 4 files changed, 389 insertions(+), 127 deletions(-) diff --git a/README.md b/README.md index 735e29c..1e8777e 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Feedr strives for constant improvement, so here's what will be implemented - YouTube Channel selector when using **/track** - Make it easier to stop tracking channels by showing channels already in the guild when doing **/untrack** - Other social media platforms - - Bluesky + - Bluesky - Make it easier to switch discord channels for uploads so that **/untrack** then **/track** is not required - **/tracked** command to show what channels are being tracked in the guild @@ -48,6 +48,13 @@ Feedr requires Bun in order to work # Changelog +## 1.4.0 + +- Added a new command! `/tracked` ([#50](https://github.com/GalvinPython/feedr/issues/50)) + - See all the tracked channels in your server + - The channel you ran the command in will appear first as there is no option to only see the current channel for now +- Locale improvments ([#43](https://github.com/GalvinPython/feedr/issues/43)) + ## 1.3.0 - Moved database to SQLite diff --git a/src/commands.ts b/src/commands.ts index 91af004..d78e490 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -1,12 +1,26 @@ -import { heapStats } from 'bun:jsc'; -import client from '.'; -import { ChannelType, GuildMember, type CommandInteraction } from 'discord.js'; -import checkIfChannelIdIsValid from './utils/youtube/checkIfChannelIdIsValid'; -import { addNewChannelToTrack, addNewGuildToTrackChannel, checkIfChannelIsAlreadyTracked, checkIfGuildIsTrackingChannelAlready, stopGuildTrackingChannel, twitchAddNewChannelToTrack, twitchAddNewGuildToTrackChannel, twitchCheckIfChannelIsAlreadyTracked, twitchCheckIfGuildIsTrackingChannelAlready, twitchStopGuildTrackingChannel } from './utils/database'; -import getChannelDetails from './utils/youtube/getChannelDetails'; -import { PermissionFlagsBits } from 'discord-api-types/v8'; -import { getStreamerId } from './utils/twitch/getStreamerId'; -import { checkIfStreamerIsLive } from './utils/twitch/checkIfStreamerIsLive'; +import { heapStats } from "bun:jsc"; +import { ChannelType, GuildMember, type CommandInteraction } from "discord.js"; +import { PermissionFlagsBits } from "discord-api-types/v8"; + +import checkIfChannelIdIsValid from "./utils/youtube/checkIfChannelIdIsValid"; +import { + addNewChannelToTrack, + addNewGuildToTrackChannel, + checkIfChannelIsAlreadyTracked, + checkIfGuildIsTrackingChannelAlready, + getAllTrackedInGuild, + stopGuildTrackingChannel, + twitchAddNewChannelToTrack, + twitchAddNewGuildToTrackChannel, + twitchCheckIfChannelIsAlreadyTracked, + twitchCheckIfGuildIsTrackingChannelAlready, + twitchStopGuildTrackingChannel, +} from "./utils/database"; +import getChannelDetails from "./utils/youtube/getChannelDetails"; +import { getStreamerId } from "./utils/twitch/getStreamerId"; +import { checkIfStreamerIsLive } from "./utils/twitch/checkIfStreamerIsLive"; + +import client from "."; interface Command { data: { @@ -23,8 +37,8 @@ const commands: Record = { ping: { data: { options: [], - name: 'ping', - description: 'Check the ping of the bot!', + name: "ping", + description: "Check the ping of the bot!", integration_types: [0, 1], contexts: [0, 1, 2], }, @@ -40,20 +54,23 @@ const commands: Record = { help: { data: { options: [], - name: 'help', - description: 'Get help on what each command does!', + name: "help", + description: "Get help on what each command does!", integration_types: [0, 1], contexts: [0, 1, 2], }, execute: async (interaction: CommandInteraction) => { await client.application?.commands?.fetch().catch(console.error); - const chat_commands = client.application?.commands.cache.map((a) => { - return `: ${a.description}`; - }); + const chat_commands = client.application?.commands.cache.map( + (a) => { + return `: ${a.description}`; + }, + ); + await interaction .reply({ ephemeral: true, - content: `Commands:\n${chat_commands?.join('\n')}`, + content: `Commands:\n${chat_commands?.join("\n")}`, }) .catch(console.error); }, @@ -61,7 +78,7 @@ const commands: Record = { sourcecode: { data: { options: [], - name: 'sourcecode', + name: "sourcecode", description: "Get the link of the app's source code.", integration_types: [0, 1], contexts: [0, 1, 2], @@ -78,8 +95,8 @@ const commands: Record = { uptime: { data: { options: [], - name: 'uptime', - description: 'Check the uptime of the bot!', + name: "uptime", + description: "Check the uptime of the bot!", integration_types: [0, 1], contexts: [0, 1, 2], }, @@ -87,9 +104,10 @@ const commands: Record = { await interaction .reply({ ephemeral: false, - content: `Uptime: ${(performance.now() / (86400 * 1000)).toFixed( - 2, - )} days`, + content: `Uptime: ${( + performance.now() / + (86400 * 1000) + ).toFixed(2)} days`, }) .catch(console.error); }, @@ -97,13 +115,14 @@ const commands: Record = { usage: { data: { options: [], - name: 'usage', - description: 'Check the heap size and disk usage of the bot!', + name: "usage", + description: "Check the heap size and disk usage of the bot!", integration_types: [0, 1], contexts: [0, 1, 2], }, execute: async (interaction: CommandInteraction) => { const heap = heapStats(); + Bun.gc(false); await interaction .reply({ @@ -113,9 +132,11 @@ const commands: Record = { heap.heapCapacity / 1024 / 1024 - ).toFixed(2)} MB (${(heap.extraMemorySize / 1024 / 1024).toFixed(2,)} MB) (${heap.objectCount.toLocaleString()} objects, ${heap.protectedObjectCount.toLocaleString()} protected-objects)`, + ).toFixed( + 2, + )} MB (${(heap.extraMemorySize / 1024 / 1024).toFixed(2)} MB) (${heap.objectCount.toLocaleString()} objects, ${heap.protectedObjectCount.toLocaleString()} protected-objects)`, ] - .join('\n') + .join("\n") .slice(0, 2000), }) .catch(console.error); @@ -125,55 +146,66 @@ const commands: Record = { data: { options: [ { - name: 'platform', - description: 'Select a supported platform to track', + name: "platform", + description: "Select a supported platform to track", type: 3, required: true, choices: [ { - name: 'Twitch', - value: 'twitch', + name: "Twitch", + value: "twitch", }, { - name: 'YouTube', - value: 'youtube', + name: "YouTube", + value: "youtube", }, - ] + ], }, { - name: 'user_id', - description: 'Enter the YouTube channel ID or Twitch Streamer to track', + name: "user_id", + description: + "Enter the YouTube channel ID or Twitch Streamer to track", type: 3, required: true, - }, { - name: 'updates_channel', - description: 'Enter the Guild channel to recieve updates in.', + }, + { + name: "updates_channel", + description: + "Enter the Guild channel to recieve updates in.", type: 7, required: true, - }, { - name: 'role', - description: 'Enter the role to mention (optional)', + }, + { + name: "role", + description: "Enter the role to mention (optional)", type: 8, required: false, - }], - name: 'track', - description: 'Track a channel to get notified when they upload a video!', + }, + ], + name: "track", + description: + "Track a channel to get notified when they upload a video!", integration_types: [0, 1], contexts: [0, 1], }, execute: async (interaction: CommandInteraction) => { // Get the YouTube Channel ID - const targetPlatform = interaction.options.get('platform')?.value as string; - const platformUserId = interaction.options.get('user_id')?.value as string; - const discordChannelId = interaction.options.get('updates_channel')?.value as string; + const targetPlatform = interaction.options.get("platform") + ?.value as string; + const platformUserId = interaction.options.get("user_id") + ?.value as string; + const discordChannelId = interaction.options.get("updates_channel") + ?.value as string; const guildId = interaction.guildId; // Checks if the platform is valid ig - if (targetPlatform != 'youtube' && targetPlatform != 'twitch') { + if (targetPlatform != "youtube" && targetPlatform != "twitch") { await interaction.reply({ ephemeral: true, - content: 'Platform not supported! Please select a platform to track!', + content: + "Platform not supported! Please select a platform to track!", }); + return; } @@ -181,93 +213,157 @@ const commands: Record = { if (!guildId || interaction.channel?.isDMBased()) { await interaction.reply({ ephemeral: true, - content: 'This command is not supported in DMs currently!\nNot a DM? Then the bot failed to get the guild info', + content: + "This command is not supported in DMs currently!\nNot a DM? Then the bot failed to get the guild info", }); + return; } // Check the permissions of the user - if (!interaction.memberPermissions?.has(PermissionFlagsBits.ManageChannels)) { + if ( + !interaction.memberPermissions?.has( + PermissionFlagsBits.ManageChannels, + ) + ) { await interaction.reply({ ephemeral: true, - content: 'You do not have the permission to manage channels!', + content: + "You do not have the permission to manage channels!", }); + return; } // 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 || targetChannel.type === ChannelType.GuildAnnouncement)) { + + 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' } + { + 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); + const botPermissions = targetChannel.permissionsFor( + client.user?.id as unknown as GuildMember, + ); const missingPermissions = requiredPermissions - .filter(permission => !botPermissions?.has(permission.flag)) - .map(permission => permission.name); + .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! Missing permissions: ${missingPermissions.join(', ')}`, + content: `The bot does not have the required permissions for the target channel! Missing permissions: ${missingPermissions.join(", ")}`, }); + return; } } else { await interaction.reply({ ephemeral: true, - content: 'The target channel is not a text channel!', + content: "The target channel is not a text channel!", }); + return; } switch (targetPlatform) { - case 'youtube': + case "youtube": // Check that the channel ID is in a valid format - if (platformUserId.length != 24 || !platformUserId.startsWith('UC')) { + if ( + platformUserId.length != 24 || + !platformUserId.startsWith("UC") + ) { await interaction.reply({ ephemeral: true, - content: 'Invalid YouTube channel ID format! Each channel ID should be 24 characters long and start with "UC". Handles are currently not supported. Need to find the channel ID? We have a guide here: https://github.com/GalvinPython/feedr/wiki/Guide:-How-to-get-the-YouTube-Channel-ID', + content: + 'Invalid YouTube channel ID format! Each channel ID should be 24 characters long and start with "UC". Handles are currently not supported. Need to find the channel ID? We have a guide here: https://github.com/GalvinPython/feedr/wiki/Guide:-How-to-get-the-YouTube-Channel-ID', }); + return; } // Check if the channel is valid - if (!await checkIfChannelIdIsValid(platformUserId)) { + if (!(await checkIfChannelIdIsValid(platformUserId))) { await interaction.reply({ ephemeral: true, - content: 'That channel doesn\'t exist!', + content: "That channel doesn't exist!", }); + return; } // Check if the channel is already being tracked in the guild - if (await checkIfGuildIsTrackingChannelAlready(platformUserId, guildId)) { + if ( + await checkIfGuildIsTrackingChannelAlready( + platformUserId, + guildId, + ) + ) { await interaction.reply({ ephemeral: true, - content: 'This channel is already being tracked!', + content: "This channel is already being tracked!", }); + return; } // Check if the channel is already being tracked globally - if (!await checkIfChannelIsAlreadyTracked(platformUserId)) { - if (!await addNewChannelToTrack(platformUserId)) { + if ( + !(await checkIfChannelIsAlreadyTracked(platformUserId)) + ) { + if (!(await addNewChannelToTrack(platformUserId))) { 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!', + content: + "An error occurred while trying to add the channel to track! This is a new channel being tracked globally, please report this error!", }); + return; } } // Add the guild to the database - if (await addNewGuildToTrackChannel(guildId, platformUserId, discordChannelId, interaction.options.get('role')?.value as string ?? null)) { - const youtubeChannelInfo = await getChannelDetails(platformUserId) + if ( + await addNewGuildToTrackChannel( + guildId, + platformUserId, + discordChannelId, + (interaction.options.get("role") + ?.value as string) ?? null, + ) + ) { + const youtubeChannelInfo = + await getChannelDetails(platformUserId); + await interaction.reply({ ephemeral: true, content: `Started tracking the channel ${youtubeChannelInfo?.channelName ?? platformUserId} in ${targetChannel.name}!`, @@ -275,45 +371,74 @@ const commands: Record = { } else { await interaction.reply({ ephemeral: true, - content: 'An error occurred while trying to add the guild to track the channel! Please report this error!', + content: + "An error occurred while trying to add the guild to track the channel! Please report this error!", }); } + return; - case 'twitch': + case "twitch": // Check if the streamer exists by getting the ID const streamerId = await getStreamerId(platformUserId); if (!streamerId) { await interaction.reply({ ephemeral: true, - content: 'That streamer doesn\'t exist!', + content: "That streamer doesn't exist!", }); + return; } // Check if the channel is already being tracked in the guild - if (await twitchCheckIfGuildIsTrackingChannelAlready(streamerId, guildId)) { + if ( + await twitchCheckIfGuildIsTrackingChannelAlready( + streamerId, + guildId, + ) + ) { await interaction.reply({ ephemeral: true, - content: 'This streamer is already being tracked!', + content: "This streamer is already being tracked!", }); + return; } // Check if the channel is already being tracked globally - if (!await twitchCheckIfChannelIsAlreadyTracked(streamerId)) { + if ( + !(await twitchCheckIfChannelIsAlreadyTracked( + streamerId, + )) + ) { const isLive = await checkIfStreamerIsLive(streamerId); - if (!await twitchAddNewChannelToTrack(streamerId, isLive)) { + + if ( + !(await twitchAddNewChannelToTrack( + streamerId, + isLive, + )) + ) { await interaction.reply({ ephemeral: true, - content: 'An error occurred while trying to add the streamer to track! This is a new streamer being tracked globally, please report this error!', + content: + "An error occurred while trying to add the streamer to track! This is a new streamer being tracked globally, please report this error!", }); + return; } } // Add the guild to the database - if (await twitchAddNewGuildToTrackChannel(guildId, streamerId, discordChannelId, interaction.options.get('role')?.value as string ?? null)) { + if ( + await twitchAddNewGuildToTrackChannel( + guildId, + streamerId, + discordChannelId, + (interaction.options.get("role") + ?.value as string) ?? null, + ) + ) { await interaction.reply({ ephemeral: true, content: `Started tracking the streamer ${platformUserId} (${streamerId}) in ${targetChannel.name}!`, @@ -321,149 +446,252 @@ const commands: Record = { } else { await interaction.reply({ ephemeral: true, - content: 'An error occurred while trying to add the guild to track the streamer! Please report this error!', + content: + "An error occurred while trying to add the guild to track the streamer! Please report this error!", }); } + return; default: - console.error('This should never happen'); + console.error("This should never happen"); break; } - } + }, }, untrack: { data: { options: [ { - name: 'platform', - description: 'Select a supported platform to track', + name: "platform", + description: "Select a supported platform to track", type: 3, required: true, choices: [ { - name: 'Twitch', - value: 'twitch', + name: "Twitch", + value: "twitch", }, { - name: 'YouTube', - value: 'youtube', + name: "YouTube", + value: "youtube", }, - ] + ], }, { - name: 'user_id', - description: 'Enter the YouTube/Twitch channel ID to stop tracking', + name: "user_id", + description: + "Enter the YouTube/Twitch channel ID to stop tracking", type: 3, required: true, - } + }, ], - name: 'untrack', - description: 'Stop a channel from being tracked in this guild!', + name: "untrack", + description: "Stop a channel from being tracked in this guild!", integration_types: [0, 1], contexts: [0, 1], }, execute: async (interaction: CommandInteraction) => { // Get the YouTube Channel ID - const youtubeChannelId = interaction.options.get('user_id')?.value as string; - const platform = interaction.options.get('platform')?.value as string; + const youtubeChannelId = interaction.options.get("user_id") + ?.value as string; + const platform = interaction.options.get("platform") + ?.value as string; const guildId = interaction.guildId; // DMs are currently not supported, so throw back an error if (!guildId || interaction.channel?.isDMBased()) { await interaction.reply({ ephemeral: true, - content: 'This command is not supported in DMs currently!\nNot a DM? Then an error has occurred :(', + content: + "This command is not supported in DMs currently!\nNot a DM? Then an error has occurred :(", }); + return; } // Check the permissions of the user - if (!interaction.memberPermissions?.has(PermissionFlagsBits.ManageChannels)) { + if ( + !interaction.memberPermissions?.has( + PermissionFlagsBits.ManageChannels, + ) + ) { await interaction.reply({ ephemeral: true, - content: 'You do not have the permission to manage channels!', + content: + "You do not have the permission to manage channels!", }); + return; } // Platform check (to shut up TS) - if (platform != 'youtube' && platform != 'twitch') { + if (platform != "youtube" && platform != "twitch") { await interaction.reply({ ephemeral: true, - content: 'Platform not supported! Please select a platform to track!', + content: + "Platform not supported! Please select a platform to track!", }); + return; } - // Remove the guild from the database switch (platform) { - case 'youtube': + case "youtube": // Check if the channel is not being tracked in the guild - if (!await checkIfGuildIsTrackingChannelAlready(youtubeChannelId, guildId)) { + if ( + !(await checkIfGuildIsTrackingChannelAlready( + youtubeChannelId, + guildId, + )) + ) { await interaction.reply({ ephemeral: true, - content: 'This channel is not being tracked in this guild!', + content: + "This channel is not being tracked in this guild!", }); + return; } - if (await stopGuildTrackingChannel(guildId, youtubeChannelId)) { + if ( + await stopGuildTrackingChannel( + guildId, + youtubeChannelId, + ) + ) { await interaction.reply({ ephemeral: true, - content: 'Successfully stopped tracking the channel!', + content: + "Successfully stopped tracking the channel!", }); } else { await interaction.reply({ ephemeral: true, - content: 'An error occurred while trying to stop tracking the channel! Please report this error!', + content: + "An error occurred while trying to stop tracking the channel! Please report this error!", }); } + return; - case 'twitch': + case "twitch": // get the twitch id for the streamer const streamerId = await getStreamerId(youtubeChannelId); + if (!streamerId) { await interaction.reply({ ephemeral: true, - content: 'An error occurred while trying to get the streamer ID! Please report this error!', + content: + "An error occurred while trying to get the streamer ID! Please report this error!", }); + return; } // check if the channel is not being tracked in the guild - if (!await twitchCheckIfGuildIsTrackingChannelAlready(streamerId, guildId)) { + if ( + !(await twitchCheckIfGuildIsTrackingChannelAlready( + streamerId, + guildId, + )) + ) { await interaction.reply({ ephemeral: true, - content: 'This streamer is not being tracked in this guild!', + content: + "This streamer is not being tracked in this guild!", }); + return; } - if (await twitchStopGuildTrackingChannel(guildId, streamerId)) { + if ( + await twitchStopGuildTrackingChannel( + guildId, + streamerId, + ) + ) { await interaction.reply({ ephemeral: true, - content: 'Successfully stopped tracking the streamer!', + content: + "Successfully stopped tracking the streamer!", }); } else { await interaction.reply({ ephemeral: true, - content: 'An error occurred while trying to stop tracking the streamer! Please report this error!', + content: + "An error occurred while trying to stop tracking the streamer! Please report this error!", }); } + return; default: return; } - } - } + }, + }, + tracked: { + data: { + options: [], + name: "tracked", + description: + "Get a list of all the channels being tracked in this guild!", + integration_types: [0, 1], + contexts: [0, 1], + }, + execute: async (interaction: CommandInteraction) => { + const guildId = interaction.guildId; + const channelId = interaction.channelId; + + if (!guildId || !channelId) { + await interaction.reply({ + ephemeral: true, + content: + "You are likely in a DM, this command is not supported in DMs!", + }); + + return; + } + + const trackedChannels = await getAllTrackedInGuild(guildId); + + if (trackedChannels.length === 0) { + await interaction.reply({ + ephemeral: true, + content: "No channels are being tracked in this guild.", + }); + + return; + } + + const filteredChannels = trackedChannels.filter( + (channel) => channel.guild_channel_id === channelId, + ); + + const newTrackedChannels = trackedChannels.filter( + (channel) => channel.guild_channel_id !== channelId, + ); + + // idk what is happening here anymore, but this is because eslint and prettier are fighting so i put them to rest by using only one line + await interaction.reply({ + ephemeral: true, + content: ` +## Tracked channels in this channel (<#${channelId}>):\n${filteredChannels.length ? filteredChannels.map((channel) => `Platform: ${channel.guild_platform} | User ID: ${channel.platform_user_id}`).join("\n") : "No channels are being tracked in this channel."} + +## Other tracked channels in this guild:\n${newTrackedChannels.map((channel) => `Platform: ${channel.guild_platform} | User ID: ${channel.platform_user_id} | Channel: <#${channel.guild_channel_id}>`).join("\n")}`, + }); + + return; + }, + }, }; // Convert commands to a Map const commandsMap = new Map(); + for (const key in commands) { if (Object.prototype.hasOwnProperty.call(commands, key)) { const command = commands[key]; - console.log('loading ' + key); + + console.log("loading " + key); commandsMap.set(key, command); } } diff --git a/src/types/database.d.ts b/src/types/database.d.ts index 2c7c265..934fb11 100644 --- a/src/types/database.d.ts +++ b/src/types/database.d.ts @@ -7,3 +7,11 @@ export interface dbTwitch { twitch_channel_id: string; is_live: boolean; } + +export type dbDiscordTable = { + guild_id: string; + guild_channel_id: string; + guild_platform: string; + platform_user_id: string; + guild_ping_role: null | string; +}; diff --git a/src/utils/database.ts b/src/utils/database.ts index da81a7c..967973b 100644 --- a/src/utils/database.ts +++ b/src/utils/database.ts @@ -1,4 +1,4 @@ -import type { dbYouTube } from "../types/database"; +import type { dbDiscordTable, dbYouTube } from "../types/database"; import path from "path"; @@ -389,3 +389,22 @@ export async function updateBotInfo( } } // #endregion + +// #region i have no idea what im doing here + +export async function getAllTrackedInGuild( + guild_id: string, +): Promise { + const query = `SELECT * FROM discord WHERE guild_id = ?`; + + try { + const statement = db.prepare(query); + const results = statement.all(guild_id); + + return results as dbDiscordTable[]; + } catch (err) { + console.error("Error getting all tracked in guild:", err); + throw err; + } +} +// #endregion