Skip to content

Commit 6c9f657

Browse files
committed
added twitch to /untrack
1 parent 6042203 commit 6c9f657

File tree

6 files changed

+166
-49
lines changed

6 files changed

+166
-49
lines changed

src/commands.ts

Lines changed: 87 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { heapStats } from 'bun:jsc';
22
import client from '.';
33
import { ChannelType, GuildMember, type CommandInteraction } from 'discord.js';
44
import checkIfChannelIdIsValid from './utils/youtube/checkIfChannelIdIsValid';
5-
import { addNewChannelToTrack, addNewGuildToTrackChannel, checkIfChannelIsAlreadyTracked, checkIfGuildIsTrackingChannelAlready, stopGuildTrackingChannel, twitchAddNewChannelToTrack, twitchAddNewGuildToTrackChannel, twitchCheckIfChannelIsAlreadyTracked, twitchCheckIfGuildIsTrackingChannelAlready } from './database';
5+
import { addNewChannelToTrack, addNewGuildToTrackChannel, checkIfChannelIsAlreadyTracked, checkIfGuildIsTrackingChannelAlready, stopGuildTrackingChannel, twitchAddNewChannelToTrack, twitchAddNewGuildToTrackChannel, twitchCheckIfChannelIsAlreadyTracked, twitchCheckIfGuildIsTrackingChannelAlready, twitchStopGuildTrackingChannel } from './database';
66
import getChannelDetails from './utils/youtube/getChannelDetails';
77
import { PermissionFlagsBits } from 'discord-api-types/v8';
88
import { getStreamerId } from './utils/twitch/getStreamerId';
@@ -231,7 +231,7 @@ const commands: Record<string, Command> = {
231231
if (platformUserId.length != 24 || !platformUserId.startsWith('UC')) {
232232
await interaction.reply({
233233
ephemeral: true,
234-
content: 'Invalid YouTube channel ID format! Each channel ID should be 24 characters long and start with "UC".',
234+
content: 'Invalid YouTube channel ID format! Each channel ID should be 24 characters long and start with "UC". Handles are currently not supported.',
235235
});
236236
return;
237237
}
@@ -292,7 +292,7 @@ const commands: Record<string, Command> = {
292292
}
293293

294294
// Check if the channel is already being tracked in the guild
295-
if (await twitchCheckIfGuildIsTrackingChannelAlready(platformUserId, guildId)) {
295+
if (await twitchCheckIfGuildIsTrackingChannelAlready(streamerId, guildId)) {
296296
await interaction.reply({
297297
ephemeral: true,
298298
content: 'This streamer is already being tracked!',
@@ -301,9 +301,9 @@ const commands: Record<string, Command> = {
301301
}
302302

303303
// Check if the channel is already being tracked globally
304-
if (!await twitchCheckIfChannelIsAlreadyTracked(platformUserId)) {
304+
if (!await twitchCheckIfChannelIsAlreadyTracked(streamerId)) {
305305
const isLive = await checkIfStreamerIsLive(streamerId);
306-
if (!await twitchAddNewChannelToTrack(platformUserId, isLive)) {
306+
if (!await twitchAddNewChannelToTrack(streamerId, isLive)) {
307307
await interaction.reply({
308308
ephemeral: true,
309309
content: 'An error occurred while trying to add the streamer to track! This is a new streamer being tracked globally, please report this error!',
@@ -313,10 +313,10 @@ const commands: Record<string, Command> = {
313313
}
314314

315315
// Add the guild to the database
316-
if (await twitchAddNewGuildToTrackChannel(guildId, platformUserId, discordChannelId, interaction.options.get('role')?.value as string ?? null)) {
316+
if (await twitchAddNewGuildToTrackChannel(guildId, streamerId, discordChannelId, interaction.options.get('role')?.value as string ?? null)) {
317317
await interaction.reply({
318318
ephemeral: true,
319-
content: `Started tracking the streamer ${platformUserId} in ${targetChannel.name}!`,
319+
content: `Started tracking the streamer ${platformUserId} (${streamerId}) in ${targetChannel.name}!`,
320320
});
321321
} else {
322322
await interaction.reply({
@@ -333,67 +333,118 @@ const commands: Record<string, Command> = {
333333
},
334334
untrack: {
335335
data: {
336-
options: [{
337-
name: 'youtube_channel',
338-
description: 'Enter the YouTube channel ID to stop tracking',
339-
type: 3,
340-
required: true,
341-
}],
336+
options: [
337+
{
338+
name: 'platform',
339+
description: 'Select a supported platform to track',
340+
type: 3,
341+
required: true,
342+
choices: [
343+
{
344+
name: 'Twitch',
345+
value: 'twitch',
346+
},
347+
{
348+
name: 'YouTube',
349+
value: 'youtube',
350+
},
351+
]
352+
},
353+
{
354+
name: 'user_id',
355+
description: 'Enter the YouTube/Twitch channel ID to stop tracking',
356+
type: 3,
357+
required: true,
358+
}
359+
],
342360
name: 'untrack',
343361
description: 'Stop a channel from being tracked in this guild!',
344362
integration_types: [0, 1],
345363
contexts: [0, 1, 2],
346364
},
347365
execute: async (interaction: CommandInteraction) => {
348366
// Get the YouTube Channel ID
349-
const youtubeChannelId = interaction.options.get('youtube_channel')?.value as string;
367+
const youtubeChannelId = interaction.options.get('user_id')?.value as string;
368+
const platform = interaction.options.get('platform')?.value as string;
350369
const guildId = interaction.guildId;
351370

352-
// Deferring the reply is not the best practice,
353-
// but in case the network/database is slow, it's better to defer the reply
354-
// so we don't get a timeout error
355-
await interaction.deferReply();
356-
357371
// DMs are currently not supported, so throw back an error
358372
if (!guildId || interaction.channel?.isDMBased()) {
359-
await interaction.followUp({
373+
await interaction.reply({
360374
ephemeral: true,
361375
content: 'This command is not supported in DMs currently!\nNot a DM? Then an error has occurred :(',
362376
});
363377
return;
364378
}
365379

366-
// First check the permissions of the user
380+
// Check the permissions of the user
367381
if (!interaction.memberPermissions?.has(PermissionFlagsBits.ManageChannels)) {
368-
await interaction.followUp({
382+
await interaction.reply({
369383
ephemeral: true,
370384
content: 'You do not have the permission to manage channels!',
371385
});
372386
return;
373387
}
374388

375-
// Check if the channel is already being tracked in the guild
376-
if (!await checkIfGuildIsTrackingChannelAlready(youtubeChannelId, guildId)) {
377-
await interaction.followUp({
389+
// Platform check (to shut up TS)
390+
if (platform != 'youtube' && platform != 'twitch') {
391+
await interaction.reply({
378392
ephemeral: true,
379-
content: 'This channel is not being tracked in this guild!',
393+
content: 'Platform not supported! Please select a platform to track!',
380394
});
381395
return;
382396
}
383397

384-
// Add the guild to the database
385-
if (await stopGuildTrackingChannel(guildId, youtubeChannelId)) {
386-
await interaction.followUp({
387-
ephemeral: true,
388-
content: 'Successfully stopped tracking the channel!',
389-
});
390-
} else {
391-
await interaction.followUp({
398+
// Check if the channel is not being tracked in the guild
399+
if (!await checkIfGuildIsTrackingChannelAlready(youtubeChannelId, guildId)) {
400+
await interaction.reply({
392401
ephemeral: true,
393-
content: 'An error occurred while trying to stop tracking the channel! Please report this error!',
402+
content: 'This channel is not being tracked in this guild!',
394403
});
404+
return;
405+
}
406+
407+
// Remove the guild from the database
408+
switch (platform) {
409+
case 'youtube':
410+
if (await stopGuildTrackingChannel(guildId, youtubeChannelId)) {
411+
await interaction.reply({
412+
ephemeral: true,
413+
content: 'Successfully stopped tracking the channel!',
414+
});
415+
} else {
416+
await interaction.reply({
417+
ephemeral: true,
418+
content: 'An error occurred while trying to stop tracking the channel! Please report this error!',
419+
});
420+
}
421+
return;
422+
case 'twitch':
423+
// get the twitch id for the streamer
424+
const streamerId = await getStreamerId(youtubeChannelId);
425+
if (!streamerId) {
426+
await interaction.reply({
427+
ephemeral: true,
428+
content: 'An error occurred while trying to get the streamer ID! Please report this error!',
429+
});
430+
return;
431+
}
432+
433+
if (await twitchStopGuildTrackingChannel(guildId, youtubeChannelId)) {
434+
await interaction.reply({
435+
ephemeral: true,
436+
content: 'Successfully stopped tracking the streamer!',
437+
});
438+
} else {
439+
await interaction.reply({
440+
ephemeral: true,
441+
content: 'An error occurred while trying to stop tracking the streamer! Please report this error!',
442+
});
443+
}
444+
return;
445+
default:
446+
return;
395447
}
396-
return;
397448
}
398449
}
399450
};

src/config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// FILL IN THIS INFORMATION IN .ENV
22
export const config: { [key: string]: string | number } = {
3-
updateInterval: process.env?.CONFIG_UPDATE_INTERVAL ? parseInt(process.env?.CONFIG_UPDATE_INTERVAL) * 1000 : 60_000,
3+
updateIntervalYouTube: process.env?.CONFIG_UPDATE_INTERVAL_YOUTUBE ? parseInt(process.env?.CONFIG_UPDATE_INTERVAL_YOUTUBE) * 1000 : 60_000,
4+
updateIntervalTwitch: process.env?.CONFIG_UPDATE_INTERVAL_TWITCH ? parseInt(process.env?.CONFIG_UPDATE_INTERVAL_TWITCH) * 1000 : 60_000,
45
}
56

67
export const env: { [key: string]: string | undefined } = {

src/events/ready.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { ActivityType, Events, PresenceUpdateStatus } from 'discord.js';
22
import client from '../index';
33
import fetchLatestUploads from '../utils/youtube/fetchLatestUploads';
44
import { config } from '../config';
5+
import { checkIfStreamersAreLive } from '../utils/twitch/checkIfStreamerIsLive';
56

67
// update the bot's presence
78
function updatePresence() {
@@ -25,5 +26,7 @@ client.once(Events.ClientReady, async (bot) => {
2526
updatePresence();
2627
fetchLatestUploads();
2728
setInterval(updatePresence, 60000);
28-
setInterval(fetchLatestUploads, config.updateInterval as number);
29+
setInterval(fetchLatestUploads, config.updateIntervalYouTube as number);
30+
checkIfStreamersAreLive();
31+
setInterval(checkIfStreamersAreLive, config.updateIntervalTwitch as number);
2932
});

src/index.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
11
// Check if all the required environment variables are set
2-
import { config, env } from './config.ts';
3-
4-
if (!config.updateInterval) {
5-
throw new Error('You MUST provide an update interval in .env!');
6-
}
2+
import { env } from './config.ts';
73

84
if (!env.discordToken || env.discordToken === 'YOUR_DISCORD_TOKEN') {
95
throw new Error('You MUST provide a discord token in .env!');

src/types/database.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
export interface dbYouTube {
22
channelId: string;
33
lastVideoId: string;
4+
}
5+
6+
export interface dbTwitch {
7+
twitch_channel_id: string;
8+
is_live: boolean;
49
}

src/utils/twitch/checkIfStreamerIsLive.ts

Lines changed: 67 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { twitchToken } from "./auth";
22
import { env } from "../../config";
3+
import { twitchGetAllChannelsToTrack, twitchGetGuildsTrackingChannel, twitchUpdateIsLive } from "../../database";
4+
import type { dbTwitch } from "../../types/database";
5+
import client from "../..";
6+
import type { TextChannel } from "discord.js";
37

48
export async function checkIfStreamerIsLive(streamerId: string): Promise<boolean> {
59
if (!twitchToken || !env.twitchClientId) {
@@ -24,11 +28,68 @@ export async function checkIfStreamerIsLive(streamerId: string): Promise<boolean
2428
return data.data.length > 0;
2529
}
2630

27-
export async function checkIfStreamersAreLive(streamerIds: string[]): Promise<string[]> {
28-
const streamers = await Promise.all(streamerIds.map(async (streamerId) => {
29-
const isLive = await checkIfStreamerIsLive(streamerId);
30-
return isLive ? streamerId : null;
31-
}));
31+
export async function checkIfStreamersAreLive(): Promise<void> {
32+
if (!twitchToken || !env.twitchClientId) {
33+
console.error("Twitch token not found in checkIfStreamersAreLive");
34+
return;
35+
}
36+
37+
const allStreamerIds = await twitchGetAllChannelsToTrack();
38+
const chunkSize = 100;
39+
const chunks = [];
40+
41+
for (let i = 0; i < allStreamerIds.length; i += chunkSize) {
42+
const chunk = allStreamerIds.slice(i, i + chunkSize);
43+
chunks.push(chunk);
44+
}
45+
46+
for (const chunk of chunks) {
47+
const urlQueries = chunk.map((streamerId: dbTwitch) => `user_id=${streamerId.twitch_channel_id}`).join("&");
48+
const res = await fetch(`https://api.twitch.tv/helix/streams?${urlQueries}`, {
49+
headers: {
50+
'Client-ID': env.twitchClientId,
51+
'Authorization': `Bearer ${twitchToken}`,
52+
},
53+
});
54+
55+
if (!res.ok) {
56+
console.error("Error fetching stream data in checkIfStreamersAreLive:", res.statusText);
57+
return;
58+
}
3259

33-
return streamers.filter((streamer) => streamer !== null) as string[];
60+
const data = await res.json();
61+
const allLiveStreamers = data.data.map((stream: any) => stream.user_id);
62+
63+
for (const streamerId of chunk) {
64+
const isLive = allLiveStreamers.includes(streamerId.twitch_channel_id);
65+
const needsUpdate = isLive !== Boolean(streamerId.is_live);
66+
let streamerName;
67+
68+
if (isLive) {
69+
streamerName = data.data.find((stream: any) => stream.user_id === streamerId.twitch_channel_id).user_name;
70+
}
71+
72+
console.log(`[Twitch] ${streamerId.twitch_channel_id} is live:`, isLive, '. Was live:', Boolean(streamerId.is_live), '. Needs update:', needsUpdate);
73+
74+
if (needsUpdate) {
75+
// Update the database
76+
console.log(`Updating ${streamerId.twitch_channel_id} to be ${isLive ? "live" : "offline"}`);
77+
await twitchUpdateIsLive(streamerId.twitch_channel_id, isLive);
78+
79+
if (isLive) {
80+
// Get all guilds that are tracking this streamer
81+
const guildsTrackingStreamer = await twitchGetGuildsTrackingChannel(streamerId.twitch_channel_id)
82+
for (const guild of guildsTrackingStreamer) {
83+
// Send a message to the channel
84+
const channel = await client.channels.fetch(guild.guild_channel_id);
85+
await (channel as TextChannel).send(
86+
guild.guild_ping_role ? `<@&${guild.guild_ping_role}> ${streamerName} is now live!` : `${streamerId.twitch_channel_id} is now live!`
87+
);
88+
}
89+
} else {
90+
console.log(`[Twitch] ${streamerId.twitch_channel_id} is offline!`);
91+
}
92+
}
93+
}
94+
}
3495
}

0 commit comments

Comments
 (0)