Add streaming notifications and improve listener loading
Adds a new /streams command for managing stream notifications and includes the streamChecker listener. Replit-Commit-Author: Agent Replit-Commit-Session-Id: aed2e46d-25bb-4b73-81a1-bb9e8437c261 Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Event-Id: e0fc1fa6-c184-478d-88a4-4fbd7c3e99ff Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3bdfff67-975a-46ad-9845-fbb6b4a4c4b5/aed2e46d-25bb-4b73-81a1-bb9e8437c261/auRwRay Replit-Helium-Checkpoint-Created: true
This commit is contained in:
parent
be66035f49
commit
2b40769784
3 changed files with 479 additions and 1 deletions
|
|
@ -720,7 +720,7 @@ if (fs.existsSync(sentinelPath)) {
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
const listenersPath = path.join(__dirname, "listeners");
|
const listenersPath = path.join(__dirname, "listeners");
|
||||||
const generalListenerFiles = ['welcome.js', 'goodbye.js', 'xpTracker.js', 'reactionXp.js', 'voiceXp.js', 'starboard.js', 'federationProtection.js'];
|
const generalListenerFiles = ['welcome.js', 'goodbye.js', 'xpTracker.js', 'reactionXp.js', 'voiceXp.js', 'starboard.js', 'federationProtection.js', 'streamChecker.js'];
|
||||||
for (const file of generalListenerFiles) {
|
for (const file of generalListenerFiles) {
|
||||||
const filePath = path.join(listenersPath, file);
|
const filePath = path.join(listenersPath, file);
|
||||||
if (fs.existsSync(filePath)) {
|
if (fs.existsSync(filePath)) {
|
||||||
|
|
|
||||||
238
aethex-bot/commands/streams.js
Normal file
238
aethex-bot/commands/streams.js
Normal file
|
|
@ -0,0 +1,238 @@
|
||||||
|
const { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits, ChannelType } = require('discord.js');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
data: new SlashCommandBuilder()
|
||||||
|
.setName('streams')
|
||||||
|
.setDescription('Twitch & YouTube live stream notifications')
|
||||||
|
.addSubcommand(sub => sub.setName('add').setDescription('Add a streamer to track')
|
||||||
|
.addStringOption(opt => opt.setName('platform').setDescription('Streaming platform')
|
||||||
|
.addChoices(
|
||||||
|
{ name: 'Twitch', value: 'twitch' },
|
||||||
|
{ name: 'YouTube', value: 'youtube' }
|
||||||
|
).setRequired(true))
|
||||||
|
.addStringOption(opt => opt.setName('username').setDescription('Streamer username or channel ID').setRequired(true))
|
||||||
|
.addChannelOption(opt => opt.setName('channel').setDescription('Channel to send notifications').setRequired(true)
|
||||||
|
.addChannelTypes(ChannelType.GuildText, ChannelType.GuildAnnouncement)))
|
||||||
|
.addSubcommand(sub => sub.setName('remove').setDescription('Remove a streamer from tracking')
|
||||||
|
.addStringOption(opt => opt.setName('username').setDescription('Streamer username').setRequired(true)))
|
||||||
|
.addSubcommand(sub => sub.setName('list').setDescription('List all tracked streamers'))
|
||||||
|
.addSubcommand(sub => sub.setName('message').setDescription('Customize the notification message')
|
||||||
|
.addStringOption(opt => opt.setName('username').setDescription('Streamer username').setRequired(true))
|
||||||
|
.addStringOption(opt => opt.setName('message').setDescription('Custom message ({streamer}, {title}, {game}, {url})').setRequired(true).setMaxLength(500)))
|
||||||
|
.addSubcommand(sub => sub.setName('test').setDescription('Test notification for a streamer')
|
||||||
|
.addStringOption(opt => opt.setName('username').setDescription('Streamer username').setRequired(true))),
|
||||||
|
|
||||||
|
async execute(interaction, supabase, client) {
|
||||||
|
if (!supabase) {
|
||||||
|
return interaction.reply({ content: 'Database not available.', ephemeral: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!interaction.member.permissions.has(PermissionFlagsBits.ManageGuild)) {
|
||||||
|
return interaction.reply({ content: 'You need Manage Server permission to manage stream notifications.', ephemeral: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const subcommand = interaction.options.getSubcommand();
|
||||||
|
|
||||||
|
if (subcommand === 'add') {
|
||||||
|
const platform = interaction.options.getString('platform');
|
||||||
|
const username = interaction.options.getString('username').toLowerCase().trim();
|
||||||
|
const channel = interaction.options.getChannel('channel');
|
||||||
|
|
||||||
|
const { data: existing } = await supabase
|
||||||
|
.from('stream_subscriptions')
|
||||||
|
.select('id')
|
||||||
|
.eq('guild_id', interaction.guildId)
|
||||||
|
.eq('username', username)
|
||||||
|
.eq('platform', platform)
|
||||||
|
.maybeSingle();
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
return interaction.reply({ content: `${username} is already being tracked on ${platform}.`, ephemeral: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { count } = await supabase
|
||||||
|
.from('stream_subscriptions')
|
||||||
|
.select('*', { count: 'exact', head: true })
|
||||||
|
.eq('guild_id', interaction.guildId);
|
||||||
|
|
||||||
|
if (count >= 25) {
|
||||||
|
return interaction.reply({ content: 'Maximum of 25 streamers per server reached.', ephemeral: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { error } = await supabase.from('stream_subscriptions').insert({
|
||||||
|
guild_id: interaction.guildId,
|
||||||
|
channel_id: channel.id,
|
||||||
|
platform,
|
||||||
|
username,
|
||||||
|
added_by: interaction.user.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error('Stream subscription error:', error);
|
||||||
|
return interaction.reply({ content: 'Failed to add streamer.', ephemeral: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const platformEmoji = platform === 'twitch' ? '<:twitch:🟣>' : '<:youtube:🔴>';
|
||||||
|
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setColor(platform === 'twitch' ? 0x9146ff : 0xff0000)
|
||||||
|
.setTitle('Streamer Added!')
|
||||||
|
.setDescription(`Now tracking **${username}** on ${platform}`)
|
||||||
|
.addFields(
|
||||||
|
{ name: 'Platform', value: platform.charAt(0).toUpperCase() + platform.slice(1), inline: true },
|
||||||
|
{ name: 'Notification Channel', value: `${channel}`, inline: true }
|
||||||
|
)
|
||||||
|
.setFooter({ text: 'You will be notified when they go live!' })
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
await interaction.reply({ embeds: [embed] });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subcommand === 'remove') {
|
||||||
|
const username = interaction.options.getString('username').toLowerCase().trim();
|
||||||
|
|
||||||
|
const { data: sub } = await supabase
|
||||||
|
.from('stream_subscriptions')
|
||||||
|
.select('id, platform')
|
||||||
|
.eq('guild_id', interaction.guildId)
|
||||||
|
.eq('username', username)
|
||||||
|
.maybeSingle();
|
||||||
|
|
||||||
|
if (!sub) {
|
||||||
|
return interaction.reply({ content: `${username} is not being tracked.`, ephemeral: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { error } = await supabase
|
||||||
|
.from('stream_subscriptions')
|
||||||
|
.delete()
|
||||||
|
.eq('id', sub.id);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return interaction.reply({ content: 'Failed to remove streamer.', ephemeral: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setColor(0xff6600)
|
||||||
|
.setTitle('Streamer Removed')
|
||||||
|
.setDescription(`No longer tracking **${username}** on ${sub.platform}`)
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
await interaction.reply({ embeds: [embed] });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subcommand === 'list') {
|
||||||
|
const { data: subs } = await supabase
|
||||||
|
.from('stream_subscriptions')
|
||||||
|
.select('*')
|
||||||
|
.eq('guild_id', interaction.guildId)
|
||||||
|
.order('created_at', { ascending: true });
|
||||||
|
|
||||||
|
if (!subs || subs.length === 0) {
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setColor(0x7c3aed)
|
||||||
|
.setTitle('Stream Subscriptions')
|
||||||
|
.setDescription('No streamers are being tracked.\nUse `/streams add` to add one!')
|
||||||
|
.setTimestamp();
|
||||||
|
return interaction.reply({ embeds: [embed] });
|
||||||
|
}
|
||||||
|
|
||||||
|
const platformEmojis = { twitch: '🟣', youtube: '🔴' };
|
||||||
|
|
||||||
|
const subList = subs.map((s, i) => {
|
||||||
|
const emoji = platformEmojis[s.platform] || '📺';
|
||||||
|
const status = s.is_live ? '🔴 LIVE' : '⚫ Offline';
|
||||||
|
return `${i + 1}. ${emoji} **${s.username}** (${s.platform})\n Channel: <#${s.channel_id}> | ${status}`;
|
||||||
|
}).join('\n\n');
|
||||||
|
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setColor(0x7c3aed)
|
||||||
|
.setTitle('Stream Subscriptions')
|
||||||
|
.setDescription(subList)
|
||||||
|
.setFooter({ text: `${subs.length}/25 slots used` })
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
await interaction.reply({ embeds: [embed] });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subcommand === 'message') {
|
||||||
|
const username = interaction.options.getString('username').toLowerCase().trim();
|
||||||
|
const customMessage = interaction.options.getString('message');
|
||||||
|
|
||||||
|
const { data: sub } = await supabase
|
||||||
|
.from('stream_subscriptions')
|
||||||
|
.select('id')
|
||||||
|
.eq('guild_id', interaction.guildId)
|
||||||
|
.eq('username', username)
|
||||||
|
.maybeSingle();
|
||||||
|
|
||||||
|
if (!sub) {
|
||||||
|
return interaction.reply({ content: `${username} is not being tracked.`, ephemeral: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { error } = await supabase
|
||||||
|
.from('stream_subscriptions')
|
||||||
|
.update({ custom_message: customMessage, updated_at: new Date().toISOString() })
|
||||||
|
.eq('id', sub.id);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return interaction.reply({ content: 'Failed to update message.', ephemeral: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setColor(0x00ff00)
|
||||||
|
.setTitle('Custom Message Set')
|
||||||
|
.setDescription(`Notification message updated for **${username}**`)
|
||||||
|
.addFields({ name: 'New Message', value: customMessage })
|
||||||
|
.setFooter({ text: 'Variables: {streamer}, {title}, {game}, {url}' })
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
await interaction.reply({ embeds: [embed] });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subcommand === 'test') {
|
||||||
|
const username = interaction.options.getString('username').toLowerCase().trim();
|
||||||
|
|
||||||
|
const { data: sub } = await supabase
|
||||||
|
.from('stream_subscriptions')
|
||||||
|
.select('*')
|
||||||
|
.eq('guild_id', interaction.guildId)
|
||||||
|
.eq('username', username)
|
||||||
|
.maybeSingle();
|
||||||
|
|
||||||
|
if (!sub) {
|
||||||
|
return interaction.reply({ content: `${username} is not being tracked.`, ephemeral: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const channel = interaction.guild.channels.cache.get(sub.channel_id);
|
||||||
|
if (!channel) {
|
||||||
|
return interaction.reply({ content: 'Notification channel not found.', ephemeral: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
let message = sub.custom_message || '🔴 **{streamer}** is now live!\n{title}\n{url}';
|
||||||
|
message = message
|
||||||
|
.replace('{streamer}', username)
|
||||||
|
.replace('{title}', 'Test Stream Title')
|
||||||
|
.replace('{game}', 'Just Chatting')
|
||||||
|
.replace('{url}', sub.platform === 'twitch' ? `https://twitch.tv/${username}` : `https://youtube.com/@${username}/live`);
|
||||||
|
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setColor(sub.platform === 'twitch' ? 0x9146ff : 0xff0000)
|
||||||
|
.setTitle(`${username} is now live!`)
|
||||||
|
.setDescription('Test Stream Title')
|
||||||
|
.addFields(
|
||||||
|
{ name: 'Game/Category', value: 'Just Chatting', inline: true },
|
||||||
|
{ name: 'Platform', value: sub.platform.charAt(0).toUpperCase() + sub.platform.slice(1), inline: true }
|
||||||
|
)
|
||||||
|
.setURL(sub.platform === 'twitch' ? `https://twitch.tv/${username}` : `https://youtube.com/@${username}/live`)
|
||||||
|
.setFooter({ text: 'TEST NOTIFICATION' })
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await channel.send({ content: message, embeds: [embed] });
|
||||||
|
await interaction.reply({ content: `Test notification sent to ${channel}!`, ephemeral: true });
|
||||||
|
} catch (err) {
|
||||||
|
await interaction.reply({ content: 'Failed to send test notification. Check bot permissions.', ephemeral: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
240
aethex-bot/listeners/streamChecker.js
Normal file
240
aethex-bot/listeners/streamChecker.js
Normal file
|
|
@ -0,0 +1,240 @@
|
||||||
|
const { EmbedBuilder } = require('discord.js');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
name: 'ready',
|
||||||
|
once: true,
|
||||||
|
|
||||||
|
async execute(client, supabase) {
|
||||||
|
if (!supabase) return;
|
||||||
|
|
||||||
|
setInterval(async () => {
|
||||||
|
await checkStreams(client, supabase);
|
||||||
|
}, 5 * 60 * 1000);
|
||||||
|
|
||||||
|
setTimeout(async () => {
|
||||||
|
await checkStreams(client, supabase);
|
||||||
|
}, 30000);
|
||||||
|
|
||||||
|
console.log('[Streams] Stream checker initialized (5 min interval)');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
async function checkStreams(client, supabase) {
|
||||||
|
try {
|
||||||
|
const { data: subs } = await supabase
|
||||||
|
.from('stream_subscriptions')
|
||||||
|
.select('*');
|
||||||
|
|
||||||
|
if (!subs || subs.length === 0) return;
|
||||||
|
|
||||||
|
const twitchSubs = subs.filter(s => s.platform === 'twitch');
|
||||||
|
const youtubeSubs = subs.filter(s => s.platform === 'youtube');
|
||||||
|
|
||||||
|
if (twitchSubs.length > 0) {
|
||||||
|
await checkTwitchStreams(client, supabase, twitchSubs);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (youtubeSubs.length > 0) {
|
||||||
|
await checkYouTubeStreams(client, supabase, youtubeSubs);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Streams] Error checking streams:', err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkTwitchStreams(client, supabase, subs) {
|
||||||
|
const twitchClientId = process.env.TWITCH_CLIENT_ID;
|
||||||
|
const twitchClientSecret = process.env.TWITCH_CLIENT_SECRET;
|
||||||
|
|
||||||
|
if (!twitchClientId || !twitchClientSecret) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tokenRes = await fetch('https://id.twitch.tv/oauth2/token', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
|
body: `client_id=${twitchClientId}&client_secret=${twitchClientSecret}&grant_type=client_credentials`
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!tokenRes.ok) return;
|
||||||
|
const { access_token } = await tokenRes.json();
|
||||||
|
|
||||||
|
const usernames = [...new Set(subs.map(s => s.username))];
|
||||||
|
const chunkedUsernames = [];
|
||||||
|
for (let i = 0; i < usernames.length; i += 100) {
|
||||||
|
chunkedUsernames.push(usernames.slice(i, i + 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const chunk of chunkedUsernames) {
|
||||||
|
const userQuery = chunk.map(u => `user_login=${u}`).join('&');
|
||||||
|
const streamsRes = await fetch(`https://api.twitch.tv/helix/streams?${userQuery}`, {
|
||||||
|
headers: {
|
||||||
|
'Client-ID': twitchClientId,
|
||||||
|
'Authorization': `Bearer ${access_token}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!streamsRes.ok) continue;
|
||||||
|
const { data: liveStreams } = await streamsRes.json();
|
||||||
|
|
||||||
|
const liveUsernames = liveStreams?.map(s => s.user_login.toLowerCase()) || [];
|
||||||
|
|
||||||
|
for (const sub of subs.filter(s => chunk.includes(s.username))) {
|
||||||
|
const isNowLive = liveUsernames.includes(sub.username.toLowerCase());
|
||||||
|
const wasLive = sub.is_live;
|
||||||
|
|
||||||
|
if (isNowLive && !wasLive) {
|
||||||
|
const streamData = liveStreams.find(s => s.user_login.toLowerCase() === sub.username.toLowerCase());
|
||||||
|
await sendNotification(client, supabase, sub, streamData, 'twitch');
|
||||||
|
|
||||||
|
await supabase
|
||||||
|
.from('stream_subscriptions')
|
||||||
|
.update({
|
||||||
|
is_live: true,
|
||||||
|
last_live_at: new Date().toISOString(),
|
||||||
|
last_notified_at: new Date().toISOString()
|
||||||
|
})
|
||||||
|
.eq('id', sub.id);
|
||||||
|
} else if (!isNowLive && wasLive) {
|
||||||
|
await supabase
|
||||||
|
.from('stream_subscriptions')
|
||||||
|
.update({ is_live: false })
|
||||||
|
.eq('id', sub.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Streams] Twitch check error:', err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkYouTubeStreams(client, supabase, subs) {
|
||||||
|
const youtubeApiKey = process.env.YOUTUBE_API_KEY;
|
||||||
|
|
||||||
|
if (!youtubeApiKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const sub of subs) {
|
||||||
|
const searchRes = await fetch(
|
||||||
|
`https://www.googleapis.com/youtube/v3/search?part=snippet&channelId=${sub.username}&type=video&eventType=live&key=${youtubeApiKey}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!searchRes.ok) {
|
||||||
|
const handleRes = await fetch(
|
||||||
|
`https://www.googleapis.com/youtube/v3/channels?part=snippet&forHandle=${sub.username}&key=${youtubeApiKey}`
|
||||||
|
);
|
||||||
|
if (handleRes.ok) {
|
||||||
|
const { items } = await handleRes.json();
|
||||||
|
if (items && items[0]) {
|
||||||
|
const channelId = items[0].id;
|
||||||
|
const liveRes = await fetch(
|
||||||
|
`https://www.googleapis.com/youtube/v3/search?part=snippet&channelId=${channelId}&type=video&eventType=live&key=${youtubeApiKey}`
|
||||||
|
);
|
||||||
|
if (liveRes.ok) {
|
||||||
|
const { items: liveItems } = await liveRes.json();
|
||||||
|
const isNowLive = liveItems && liveItems.length > 0;
|
||||||
|
const wasLive = sub.is_live;
|
||||||
|
|
||||||
|
if (isNowLive && !wasLive) {
|
||||||
|
await sendNotification(client, supabase, sub, liveItems[0], 'youtube');
|
||||||
|
await supabase
|
||||||
|
.from('stream_subscriptions')
|
||||||
|
.update({
|
||||||
|
is_live: true,
|
||||||
|
last_live_at: new Date().toISOString(),
|
||||||
|
last_notified_at: new Date().toISOString()
|
||||||
|
})
|
||||||
|
.eq('id', sub.id);
|
||||||
|
} else if (!isNowLive && wasLive) {
|
||||||
|
await supabase
|
||||||
|
.from('stream_subscriptions')
|
||||||
|
.update({ is_live: false })
|
||||||
|
.eq('id', sub.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { items } = await searchRes.json();
|
||||||
|
const isNowLive = items && items.length > 0;
|
||||||
|
const wasLive = sub.is_live;
|
||||||
|
|
||||||
|
if (isNowLive && !wasLive) {
|
||||||
|
await sendNotification(client, supabase, sub, items[0], 'youtube');
|
||||||
|
await supabase
|
||||||
|
.from('stream_subscriptions')
|
||||||
|
.update({
|
||||||
|
is_live: true,
|
||||||
|
last_live_at: new Date().toISOString(),
|
||||||
|
last_notified_at: new Date().toISOString()
|
||||||
|
})
|
||||||
|
.eq('id', sub.id);
|
||||||
|
} else if (!isNowLive && wasLive) {
|
||||||
|
await supabase
|
||||||
|
.from('stream_subscriptions')
|
||||||
|
.update({ is_live: false })
|
||||||
|
.eq('id', sub.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise(r => setTimeout(r, 1000));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Streams] YouTube check error:', err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendNotification(client, supabase, sub, streamData, platform) {
|
||||||
|
try {
|
||||||
|
const guild = client.guilds.cache.get(sub.guild_id);
|
||||||
|
if (!guild) return;
|
||||||
|
|
||||||
|
const channel = guild.channels.cache.get(sub.channel_id);
|
||||||
|
if (!channel) return;
|
||||||
|
|
||||||
|
let title, game, url, thumbnail;
|
||||||
|
|
||||||
|
if (platform === 'twitch') {
|
||||||
|
title = streamData.title || 'Live Stream';
|
||||||
|
game = streamData.game_name || 'Unknown';
|
||||||
|
url = `https://twitch.tv/${sub.username}`;
|
||||||
|
thumbnail = streamData.thumbnail_url?.replace('{width}', '400').replace('{height}', '225');
|
||||||
|
} else {
|
||||||
|
title = streamData.snippet?.title || 'Live Stream';
|
||||||
|
game = streamData.snippet?.channelTitle || sub.username;
|
||||||
|
url = `https://youtube.com/watch?v=${streamData.id?.videoId}`;
|
||||||
|
thumbnail = streamData.snippet?.thumbnails?.high?.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
let message = sub.custom_message || '🔴 **{streamer}** is now live!\n{title}\n{url}';
|
||||||
|
message = message
|
||||||
|
.replace('{streamer}', sub.username)
|
||||||
|
.replace('{title}', title)
|
||||||
|
.replace('{game}', game)
|
||||||
|
.replace('{url}', url);
|
||||||
|
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setColor(platform === 'twitch' ? 0x9146ff : 0xff0000)
|
||||||
|
.setTitle(`${sub.username} is now live!`)
|
||||||
|
.setDescription(title)
|
||||||
|
.setURL(url)
|
||||||
|
.addFields(
|
||||||
|
{ name: 'Game/Category', value: game, inline: true },
|
||||||
|
{ name: 'Platform', value: platform.charAt(0).toUpperCase() + platform.slice(1), inline: true }
|
||||||
|
)
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
if (thumbnail) {
|
||||||
|
embed.setImage(thumbnail);
|
||||||
|
}
|
||||||
|
|
||||||
|
await channel.send({ content: message, embeds: [embed] });
|
||||||
|
console.log(`[Streams] Sent notification for ${sub.username} going live on ${platform}`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Streams] Notification send error:', err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue