From 16b5389dfade1979a91457e48b6b5f8b6f01e4cd Mon Sep 17 00:00:00 2001 From: Joseph Shenton Date: Fri, 19 Dec 2025 14:26:19 +1100 Subject: [PATCH 01/12] feat: add configurable Plex client identifier and token header - Add PLEX_CLIENT_IDENTIFIER config option (defaults to 'SchroStream') - Include X-Plex-Token in FFmpeg HTTP headers for authenticated requests - Makes client identifier configurable instead of hardcoded --- src/config.ts | 1 + src/stream/video-streamer.ts | 3 ++- src/types/index.ts | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/config.ts b/src/config.ts index 922d9d0..0f7f836 100644 --- a/src/config.ts +++ b/src/config.ts @@ -31,6 +31,7 @@ export const config: Config = { plex: { url: getEnvOrThrow('PLEX_URL').replace(/\/$/, ''), token: getEnvOrThrow('PLEX_TOKEN'), + clientIdentifier: getEnvOrDefault('PLEX_CLIENT_IDENTIFIER', 'SchroStream'), }, stream: { defaultQuality: parseInt(getEnvOrDefault('DEFAULT_QUALITY', '1080'), 10), diff --git a/src/stream/video-streamer.ts b/src/stream/video-streamer.ts index 3f3c45c..f127255 100644 --- a/src/stream/video-streamer.ts +++ b/src/stream/video-streamer.ts @@ -77,11 +77,12 @@ class VideoStreamer { // Build headers string for FFmpeg (must be before -i) const headers = [ 'Accept: application/json', - 'X-Plex-Client-Identifier: SchroStream', + 'X-Plex-Client-Identifier: ' + config.plex.clientIdentifier, 'X-Plex-Product: Plex Web', 'X-Plex-Version: 4.0', 'X-Plex-Platform: Chrome', 'X-Plex-Device: Linux', + 'X-Plex-Token: ' + config.plex.token, ].join('\r\n') + '\r\n'; const ffmpegArgs = [ diff --git a/src/types/index.ts b/src/types/index.ts index 73643ba..0f9f264 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -75,6 +75,7 @@ export interface Config { plex: { url: string; token: string; + clientIdentifier: string; }; stream: { defaultQuality: number; From fc27a59805ac171f7abb6732b61fc64733129e18 Mon Sep 17 00:00:00 2001 From: Joseph Shenton Date: Fri, 19 Dec 2025 14:27:41 +1100 Subject: [PATCH 02/12] feat: add multi-user and role support with help command - ALLOWED_USERS: comma-separated list of user IDs who can use the bot - ALLOWED_ROLES: comma-separated list of role IDs that grant access - Added !help command to show all available commands - Updated .env.example with new config options - Bot account always has access, plus any allowed users/roles --- .env.example | 12 ++++++++--- src/bot/client.ts | 42 ++++++++++++++++++++++++++++++++------- src/bot/commands/help.ts | 30 ++++++++++++++++++++++++++++ src/bot/commands/index.ts | 4 ++++ src/config.ts | 12 +++++------ src/types/index.ts | 3 ++- 6 files changed, 86 insertions(+), 17 deletions(-) create mode 100644 src/bot/commands/help.ts diff --git a/.env.example b/.env.example index 523139b..f525db9 100644 --- a/.env.example +++ b/.env.example @@ -2,9 +2,15 @@ # WARNING: Using self-bots violates Discord ToS - use at your own risk DISCORD_TOKEN=your_discord_user_token_here -# Owner ID - Your MAIN account's user ID (the account that controls the bot) -# Right-click your profile → Copy User ID (requires Developer Mode) -OWNER_ID=your_main_account_user_id_here +# Allowed Users - Comma-separated user IDs who can control the bot +# Right-click user → Copy User ID (requires Developer Mode) +# Leave empty to only allow the bot's own account +ALLOWED_USERS= + +# Allowed Roles - Comma-separated role IDs that can control the bot +# Right-click role → Copy Role ID (requires Developer Mode) +# Users with ANY of these roles can use the bot +ALLOWED_ROLES= # Allowed Guilds - Comma-separated server IDs (leave empty for all servers) # Right-click server → Copy Server ID (requires Developer Mode) diff --git a/src/bot/client.ts b/src/bot/client.ts index fb6affc..23f99e0 100644 --- a/src/bot/client.ts +++ b/src/bot/client.ts @@ -10,20 +10,48 @@ export const client = new Client({ client.on('ready', async () => { console.log(`[SchroStream] Logged in as ${client.user?.tag}`); console.log(`[SchroStream] Prefix: ${config.discord.prefix}`); - console.log(`[SchroStream] Owner: ${config.discord.ownerId || client.user?.id} (${config.discord.ownerId ? 'external' : 'self'})`); + + if (config.discord.allowedUsers.length > 0) { + console.log(`[SchroStream] Allowed users: ${config.discord.allowedUsers.join(', ')}`); + } else { + console.log(`[SchroStream] Allowed users: ${client.user?.id} (self only)`); + } + + if (config.discord.allowedRoles.length > 0) { + console.log(`[SchroStream] Allowed roles: ${config.discord.allowedRoles.join(', ')}`); + } + console.log(`[SchroStream] Allowed guilds: ${config.discord.allowedGuilds.length > 0 ? config.discord.allowedGuilds.join(', ') : 'all'}`); console.log(`[SchroStream] Video streaming enabled (Go Live)`); initVideoStreamer(client); }); -client.on('messageCreate', async (message) => { - const isOwner = config.discord.ownerId - ? message.author.id === config.discord.ownerId - : message.author.id === client.user?.id; +function hasPermission(message: import('discord.js-selfbot-v13').Message): boolean { + // Always allow the bot's own account + if (message.author.id === client.user?.id) return true; + + // Check if user is in the allowed users list + if (config.discord.allowedUsers.length > 0) { + if (config.discord.allowedUsers.includes(message.author.id)) return true; + } + + // Check if user has any of the allowed roles (only in guilds) + if (config.discord.allowedRoles.length > 0 && message.guild && message.member) { + const memberRoles = message.member.roles.cache; + const hasAllowedRole = config.discord.allowedRoles.some(roleId => memberRoles.has(roleId)); + if (hasAllowedRole) return true; + } + + // If no users or roles configured, only bot account can use + return config.discord.allowedUsers.length === 0 && config.discord.allowedRoles.length === 0; +} - if (!isOwner) return; +client.on('messageCreate', async (message) => { + // Check user permissions + if (!hasPermission(message)) return; + // Check guild restrictions if (config.discord.allowedGuilds.length > 0 && message.guild) { if (!config.discord.allowedGuilds.includes(message.guild.id)) return; } @@ -39,7 +67,7 @@ client.on('messageCreate', async (message) => { await handleCommand(commandName, message, args); } catch (error) { console.error(`[Command Error] ${commandName}:`, error); - await message.edit(`❌ Error: ${error instanceof Error ? error.message : 'Unknown error'}`).catch(() => {}); + await message.channel.send(`❌ Error: ${error instanceof Error ? error.message : 'Unknown error'}`).catch(() => {}); } }); diff --git a/src/bot/commands/help.ts b/src/bot/commands/help.ts new file mode 100644 index 0000000..f5eaa0c --- /dev/null +++ b/src/bot/commands/help.ts @@ -0,0 +1,30 @@ +import type { Message } from 'discord.js-selfbot-v13'; +import config from '../../config.js'; + +export async function helpCommand(message: Message, _args: string[]): Promise { + const prefix = config.discord.prefix; + + const helpText = ` +**📺 SchroStream Commands** + +**Media Search & Playback:** +\`${prefix}search \` - Search for movies or TV shows +\`${prefix}play \` - Play a result from your search +\`${prefix}stop\` - Stop the current stream + +**Playback Controls:** +\`${prefix}pause\` - Pause/resume playback +\`${prefix}seek