From 8323212ad090c70c217232d13b12d955f540d819 Mon Sep 17 00:00:00 2001 From: Eric Wehrly <1851833+EricWehrly@users.noreply.github.com> Date: Sat, 1 Mar 2025 01:11:26 -0500 Subject: [PATCH 1/2] slack chatbot that connects --- .env.example | 4 + .gitignore | 3 +- Dockerfile | 11 ++ docker-compose.yml | 10 ++ machines/media/docker-compose.yml | 7 ++ machines/media/services/jellyfin.yml | 15 --- machines/pi/docker-compose.yml | 4 + machines/pi/services/homeassistant.yml | 13 -- machines/pi/services/pihole.yml | 24 ---- services/jellyfin-slackbot/.env.example | 4 + services/jellyfin-slackbot/Dockerfile | 11 ++ services/jellyfin-slackbot/README.md | 114 ++++++++++++++++++ services/jellyfin-slackbot/docker-compose.yml | 11 ++ .../jellyfin-slackbot/jellyseer-api-notes.md | 32 +++++ services/jellyfin-slackbot/package.json | 17 +++ services/jellyfin-slackbot/references.md | 5 + .../jellyfin-slackbot/slack-app-manifest.json | 38 ++++++ services/jellyfin-slackbot/src/index.js | 45 +++++++ .../jellyfin-slackbot/src/jellyseerClient.js | 31 +++++ .../jellyfin-slackbot/src/requestHandler.js | 28 +++++ services/jellyfin-slackbot/src/slackApp.js | 67 ++++++++++ services/jellyfin-slackbot/src/validators.js | 48 ++++++++ .../tests/requestHandler.test.js | 14 +++ src/index.js | 23 ++++ src/requestHandler.js | 28 +++++ tests/requestHandler.test.js | 14 +++ 26 files changed, 568 insertions(+), 53 deletions(-) create mode 100644 .env.example create mode 100644 Dockerfile create mode 100644 docker-compose.yml create mode 100644 machines/media/docker-compose.yml delete mode 100644 machines/media/services/jellyfin.yml delete mode 100644 machines/pi/services/homeassistant.yml delete mode 100644 machines/pi/services/pihole.yml create mode 100644 services/jellyfin-slackbot/.env.example create mode 100644 services/jellyfin-slackbot/Dockerfile create mode 100644 services/jellyfin-slackbot/README.md create mode 100644 services/jellyfin-slackbot/docker-compose.yml create mode 100644 services/jellyfin-slackbot/jellyseer-api-notes.md create mode 100644 services/jellyfin-slackbot/package.json create mode 100644 services/jellyfin-slackbot/references.md create mode 100644 services/jellyfin-slackbot/slack-app-manifest.json create mode 100644 services/jellyfin-slackbot/src/index.js create mode 100644 services/jellyfin-slackbot/src/jellyseerClient.js create mode 100644 services/jellyfin-slackbot/src/requestHandler.js create mode 100644 services/jellyfin-slackbot/src/slackApp.js create mode 100644 services/jellyfin-slackbot/src/validators.js create mode 100644 services/jellyfin-slackbot/tests/requestHandler.test.js create mode 100644 src/index.js create mode 100644 src/requestHandler.js create mode 100644 tests/requestHandler.test.js diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..4ed08ee --- /dev/null +++ b/.env.example @@ -0,0 +1,4 @@ +# Example environment variables for the Slack bot + +SLACK_BOT_TOKEN=your-slack-bot-token +JELLYSEERR_API_URL=your-jellyseerr-api-url diff --git a/.gitignore b/.gitignore index f0d96d2..51a890f 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -.vscode/sftp.json \ No newline at end of file +.vscode/sftp.json +.env \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..073b89b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +FROM node:14 + +WORKDIR /app + +COPY package*.json ./ + +RUN npm install + +COPY . . + +CMD ["node", "src/index.js"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a1d1422 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,10 @@ +version: '3' + +services: + slack-bot: + build: . + environment: + - SLACK_BOT_TOKEN=${SLACK_BOT_TOKEN} + - JELLYSEERR_API_URL=${JELLYSEERR_API_URL} + ports: + - "3000:3000" diff --git a/machines/media/docker-compose.yml b/machines/media/docker-compose.yml new file mode 100644 index 0000000..8d599e6 --- /dev/null +++ b/machines/media/docker-compose.yml @@ -0,0 +1,7 @@ +version: "3" + +services: + jellyfin-slackbot: + extends: + file: ./services/jellyfin-slackbot.yml + service: slack-bot diff --git a/machines/media/services/jellyfin.yml b/machines/media/services/jellyfin.yml deleted file mode 100644 index 24d764f..0000000 --- a/machines/media/services/jellyfin.yml +++ /dev/null @@ -1,15 +0,0 @@ -version: "3" - -services: - jellyfin: - image: jellyfin/jellyfin - container_name: jellyfin - environment: - - TZ=America/New_York - volumes: - - /path/to/config:/config - - /path/to/cache:/cache - - /path/to/media:/media - ports: - - 8096:8096 - restart: unless-stopped diff --git a/machines/pi/docker-compose.yml b/machines/pi/docker-compose.yml index 0b9a436..1e69e54 100644 --- a/machines/pi/docker-compose.yml +++ b/machines/pi/docker-compose.yml @@ -9,3 +9,7 @@ services: extends: file: ./services/homeassistant.yml service: homeassistant + jellyfin-slackbot: + extends: + file: ./services/jellyfin-slackbot.yml + service: slack-bot diff --git a/machines/pi/services/homeassistant.yml b/machines/pi/services/homeassistant.yml deleted file mode 100644 index dbb0ddf..0000000 --- a/machines/pi/services/homeassistant.yml +++ /dev/null @@ -1,13 +0,0 @@ -version: "3" - -services: - homeassistant: - container_name: home-assistant - image: homeassistant/home-assistant:stable - volumes: - - /etc/homeassistant:/config - environment: - - TZ=America/New_York - restart: always - privileged: true - network_mode: host diff --git a/machines/pi/services/pihole.yml b/machines/pi/services/pihole.yml deleted file mode 100644 index c0d7e59..0000000 --- a/machines/pi/services/pihole.yml +++ /dev/null @@ -1,24 +0,0 @@ -version: "3" - -services: - pihole: - container_name: pihole - image: pihole/pihole:latest - ports: - - "53:53/tcp" - - "53:53/udp" - - "67:67/udp" - - "80:80/tcp" - - "443:443/tcp" - environment: - TZ: 'America/New_York' - volumes: - - './etc-pihole/:/etc/pihole/' - - './etc-dnsmasq.d/:/etc/dnsmasq.d/' - - '/var/www/index.html:/var/www/html/index.html' - dns: - - 127.0.0.1 - - 1.1.1.1 - cap_add: - - NET_ADMIN - restart: unless-stopped diff --git a/services/jellyfin-slackbot/.env.example b/services/jellyfin-slackbot/.env.example new file mode 100644 index 0000000..4ed08ee --- /dev/null +++ b/services/jellyfin-slackbot/.env.example @@ -0,0 +1,4 @@ +# Example environment variables for the Slack bot + +SLACK_BOT_TOKEN=your-slack-bot-token +JELLYSEERR_API_URL=your-jellyseerr-api-url diff --git a/services/jellyfin-slackbot/Dockerfile b/services/jellyfin-slackbot/Dockerfile new file mode 100644 index 0000000..073b89b --- /dev/null +++ b/services/jellyfin-slackbot/Dockerfile @@ -0,0 +1,11 @@ +FROM node:14 + +WORKDIR /app + +COPY package*.json ./ + +RUN npm install + +COPY . . + +CMD ["node", "src/index.js"] diff --git a/services/jellyfin-slackbot/README.md b/services/jellyfin-slackbot/README.md new file mode 100644 index 0000000..83b0721 --- /dev/null +++ b/services/jellyfin-slackbot/README.md @@ -0,0 +1,114 @@ +## Slack Bot Setup Instructions + +### Slack Bot Creation + +1. Go to the [Slack API](https://api.slack.com/apps) and create a new app. +2. Choose a name for your app and select the workspace where you want to install it. +3. Once the app is created, navigate to the "OAuth & Permissions" section. +4. Add the following bot token scopes: + - `channels:history` + - `channels:read` + - `chat:write` + - `groups:history` + - `groups:read` + - `im:history` + - `im:read` + - `mpim:history` + - `mpim:read` +5. Install the app to your workspace and copy the Bot User OAuth Token. +6. In the "Basic Information" section, locate the "Signing Secret" under "App Credentials". + Copy this value as you'll need it for the configuration. + +### Required Permissions & OAuth Setup + +1. In the "OAuth & Permissions" section, ensure that the required bot token scopes are added. +2. Copy the Bot User OAuth Token and add it to your `.env` file as `SLACK_BOT_TOKEN`. + +### Configuration Using `.env` File + +1. Create a `.env` file in the root directory of the project. +2. Add the following environment variables to the `.env` file: + ``` + SLACK_BOT_TOKEN=your-slack-bot-token + SLACK_SIGNING_SECRET=your-slack-signing-secret + JELLYSEERR_API_URL=your-jellyseerr-api-url + JELLYSEERR_API_KEY=your-jellyseerr-api-key + ``` + +### Environment Variables + +The following environment variables are required: + +| Variable | Description | +|----------|-------------| +| SLACK_BOT_TOKEN | OAuth token starting with `xoxb-`. Found under "OAuth & Permissions" in your Slack app settings. Required for the bot to interact with Slack's APIs. | +| SLACK_SIGNING_SECRET | Used to verify requests are coming from Slack. Found under "Basic Information" in your Slack app settings. | +| JELLYSEERR_API_URL | Base URL for your Jellyseerr instance (e.g., `http://localhost:5055`). | +| JELLYSEERR_API_KEY | API key for Jellyseerr, generated in the Jellyseerr settings. | + +Optional environment variables: + +| Variable | Description | +|----------|-------------| +| SLACK_APP_TOKEN | Token starting with `xapp-`. **Only required if using Socket Mode**. Found under "Basic Information" in your Slack app settings. | +| PORT | Port for the HTTP server to listen on (default: 3000). Not needed if using Socket Mode. | + +### Connection Modes + +The bot supports two connection modes: + +1. **HTTP Mode (default)** - Requires a public URL with proper request URLs configured in the Slack app settings +2. **Socket Mode** - No public URL required, but needs the additional SLACK_APP_TOKEN + +To use Socket Mode, set `socketMode: true` in the App initialization and ensure you've set the SLACK_APP_TOKEN environment variable. + +--- + +## Running the Slack Bot Using Docker and Docker Compose + +### Docker Setup + +1. Create a `Dockerfile` in the root directory of the project with the following content: + ``` + FROM node:14 + + WORKDIR /app + + COPY package*.json ./ + + RUN npm install + + COPY . . + + CMD ["node", "src/index.js"] + ``` + +2. Create a `docker-compose.yml` file in the root directory of the project with the following content: + ``` + version: '3' + + services: + slack-bot: + build: . + environment: + - SLACK_BOT_TOKEN=${SLACK_BOT_TOKEN} + - SLACK_SIGNING_SECRET=${SLACK_SIGNING_SECRET} + - JELLYSEERR_API_URL=${JELLYSEERR_API_URL} + - JELLYSEERR_API_KEY=${JELLYSEERR_API_KEY} + ports: + - "3000:3000" + ``` + +### Running the Slack Bot + +1. Build the Docker image: + ``` + docker-compose build + ``` + +2. Start the Slack bot: + ``` + docker-compose up + ``` + +3. The Slack bot should now be running and listening for messages in the configured channel. diff --git a/services/jellyfin-slackbot/docker-compose.yml b/services/jellyfin-slackbot/docker-compose.yml new file mode 100644 index 0000000..c963126 --- /dev/null +++ b/services/jellyfin-slackbot/docker-compose.yml @@ -0,0 +1,11 @@ +services: + slack-bot: + build: . + environment: + - SLACK_BOT_TOKEN=${SLACK_BOT_TOKEN} + - SLACK_SIGNING_SECRET=${SLACK_SIGNING_SECRET} + - SLACK_APP_TOKEN=${SLACK_APP_TOKEN} + - JELLYSEERR_API_URL=${JELLYSEERR_API_URL} + - JELLYSEERR_API_KEY=${JELLYSEERR_API_KEY} + ports: + - "3000:3000" diff --git a/services/jellyfin-slackbot/jellyseer-api-notes.md b/services/jellyfin-slackbot/jellyseer-api-notes.md new file mode 100644 index 0000000..a0adcdc --- /dev/null +++ b/services/jellyfin-slackbot/jellyseer-api-notes.md @@ -0,0 +1,32 @@ +# Jellyseerr API Notes + +## Authentication +- Requires API key in header: `X-Api-Key` +- API key generated in Jellyseerr settings / general + +## Relevant Endpoints + +### Search Media +- `GET /api/v1/search` +- Query params: + - `query` (string, required): Search term + - `page` (integer, optional): Page number + - `language` (string, optional): ISO 639-1 language code + +### Request Media +- `POST /api/v1/request` +- Required body parameters: + - `mediaType`: "movie" or "tv" + - `mediaId`: TMDB ID of the media +- Optional parameters: + - `seasons`: Array of season numbers (for TV) + - `tvdbId`: TVDB ID (for TV shows) + +## Example Flow +1. Search for media using title +2. Get TMDB ID from search results +3. Use TMDB ID to make request + +## Required Environment Variables +- `JELLYSEERR_API_URL`: Base URL of Jellyseerr instance +- `JELLYSEERR_API_KEY`: API key from settings \ No newline at end of file diff --git a/services/jellyfin-slackbot/package.json b/services/jellyfin-slackbot/package.json new file mode 100644 index 0000000..bef83d0 --- /dev/null +++ b/services/jellyfin-slackbot/package.json @@ -0,0 +1,17 @@ +{ + "name": "jellyfin-slackbot", + "version": "1.0.0", + "description": "Slack bot for Jellyfin requests", + "main": "src/index.js", + "scripts": { + "start": "node src/index.js", + "test": "jest" + }, + "dependencies": { + "@slack/bolt": "^3.0.0", + "axios": "^0.21.1" + }, + "devDependencies": { + "jest": "^27.0.0" + } +} \ No newline at end of file diff --git a/services/jellyfin-slackbot/references.md b/services/jellyfin-slackbot/references.md new file mode 100644 index 0000000..b85946a --- /dev/null +++ b/services/jellyfin-slackbot/references.md @@ -0,0 +1,5 @@ +https://api-docs.jellyseerr.com/ + +https://api-docs.jellyseerr.com/#request-media + +https://api-docs.jellyseerr.com/#search \ No newline at end of file diff --git a/services/jellyfin-slackbot/slack-app-manifest.json b/services/jellyfin-slackbot/slack-app-manifest.json new file mode 100644 index 0000000..2356e0a --- /dev/null +++ b/services/jellyfin-slackbot/slack-app-manifest.json @@ -0,0 +1,38 @@ +{ + "display_information": { + "name": "Jellyfin Request Bot", + "description": "A Slack bot for managing Jellyfin media requests", + "background_color": "#2c3e50" + }, + "features": { + "bot_user": { + "display_name": "Jellyfin Bot", + "always_online": true + }, + "app_home": { + "home_tab_enabled": true, + "messages_tab_enabled": true + } + }, + "oauth_config": { + "scopes": { + "bot": [ + "channels:history", + "channels:read", + "chat:write", + "groups:history", + "groups:read", + "im:history", + "im:read", + "mpim:history", + "mpim:read" + ] + } + }, + "settings": { + "socket_mode_enabled": true, + "interactivity": { + "is_enabled": true + } + } +} \ No newline at end of file diff --git a/services/jellyfin-slackbot/src/index.js b/services/jellyfin-slackbot/src/index.js new file mode 100644 index 0000000..54ddf4e --- /dev/null +++ b/services/jellyfin-slackbot/src/index.js @@ -0,0 +1,45 @@ +const { createSlackApp, updateBotStatus } = require('./slackApp'); +const { checkJellyseerrHealth } = require('./jellyseerClient'); + +const requiredEnvVars = [ + 'SLACK_BOT_TOKEN', + 'SLACK_APP_TOKEN', + 'SLACK_SIGNING_SECRET', + 'JELLYSEERR_API_URL', + 'JELLYSEERR_API_KEY' +]; + +// Add global unhandled promise rejection handler +process.on('unhandledRejection', (reason, promise) => { + console.error('🔴 Unhandled Promise Rejection:', reason); + // Optionally log the promise that caused the rejection + console.error('Promise:', promise); +}); + +function validateEnvironment() { + const missing = requiredEnvVars.filter(varName => !process.env[varName]?.length); + if (missing.length) { + throw new Error(`Missing environment variables: ${missing.join(', ')}`); + } +} + +// Start the app +(async () => { + console.log('🚀 Starting server...'); + try { + validateEnvironment(); + await checkJellyseerrHealth(); + + const app = createSlackApp(); + + // Comment out the status update function call since it's causing errors + // await updateBotStatus(app, "Online", ":white_check_mark:"); + + console.log('🌐 Starting Slack app listener...'); + await app.start(process.env.PORT || 3000); + console.log('✅ Bot is running!'); + } catch (error) { + console.error('🔴 Failed to start app:', error); + process.exit(1); + } +})(); diff --git a/services/jellyfin-slackbot/src/jellyseerClient.js b/services/jellyfin-slackbot/src/jellyseerClient.js new file mode 100644 index 0000000..dd5d427 --- /dev/null +++ b/services/jellyfin-slackbot/src/jellyseerClient.js @@ -0,0 +1,31 @@ +const axios = require('axios'); + +// Reference: https://github.com/fallenbagel/jellyseerr/blob/master/server/routes/status.ts +async function checkJellyseerrHealth() { + try { + // Using status endpoint as it requires auth and is lightweight + const response = await axios.get(`${process.env.JELLYSEERR_API_URL}/api/v1/status`, { + headers: { 'X-Api-Key': process.env.JELLYSEERR_API_KEY }, + timeout: 5000 // 5 second timeout + }); + console.log('✅ Jellyseerr connection test successful'); + return true; + } catch (error) { + if (error.code === 'ECONNREFUSED' || error.code === 'ETIMEDOUT') { + console.error('❌ Jellyseerr server unreachable:', error.message); + } else if (error.response) { + console.error('❌ Jellyseerr API error:', { + status: error.response.status, + statusText: error.response.statusText, + data: error.response.data + }); + } else { + console.error('❌ Unknown error:', error.message); + } + throw error; + } +} + +module.exports = { + checkJellyseerrHealth +}; diff --git a/services/jellyfin-slackbot/src/requestHandler.js b/services/jellyfin-slackbot/src/requestHandler.js new file mode 100644 index 0000000..b22c7cc --- /dev/null +++ b/services/jellyfin-slackbot/src/requestHandler.js @@ -0,0 +1,28 @@ +const axios = require('axios'); + +async function handleRequest(message) { + const videoRequest = parseMessage(message); + const response = await forwardToJellyseerr(videoRequest); + return response; +} + +function parseMessage(message) { + // Extract video request details from the message + // This is a placeholder implementation, adjust as needed + return { + title: message.split('request video ')[1] || message + }; +} + +async function forwardToJellyseerr(videoRequest) { + try { + const response = await axios.post(process.env.JELLYSEERR_API_URL, videoRequest); + return response.data; + } catch (error) { + throw new Error('Failed to forward request to Jellyseerr'); + } +} + +module.exports = { + handleRequest +}; diff --git a/services/jellyfin-slackbot/src/slackApp.js b/services/jellyfin-slackbot/src/slackApp.js new file mode 100644 index 0000000..a0449d6 --- /dev/null +++ b/services/jellyfin-slackbot/src/slackApp.js @@ -0,0 +1,67 @@ +const { App } = require('@slack/bolt'); +const requestHandler = require('./requestHandler'); + +function createSlackApp() { + console.log('📱 Initializing Slack app...'); + + // Log token info for debugging (only first few chars for security) + const botTokenPreview = process.env.SLACK_BOT_TOKEN ? + `${process.env.SLACK_BOT_TOKEN.substring(0, 5)}...` : 'undefined'; + const appTokenPreview = process.env.SLACK_APP_TOKEN ? + `${process.env.SLACK_APP_TOKEN.substring(0, 5)}...` : 'undefined'; + console.log(`Bot token preview: ${botTokenPreview}`); + console.log(`App token preview: ${appTokenPreview}`); + + const app = new App({ + token: process.env.SLACK_BOT_TOKEN, + signingSecret: process.env.SLACK_SIGNING_SECRET, + socketMode: true, // Changed to true to use Socket Mode + appToken: process.env.SLACK_APP_TOKEN, // Only needed for Socket Mode + }); + + // Add error handler before other listeners + app.error(async (error) => { + console.error('🔴 Slack app error:', error); + }); + + app.message(async ({ message, say, logger }) => { + console.log('📨 Received message:', JSON.stringify(message)); + + try { + if (message.text) { + console.log('🎬 Processing video request...'); + try { + const response = await requestHandler.handleRequest(message.text); + console.log('✅ Request handled successfully, responding to user'); + await say(`Request received: ${response}`); + } catch (error) { + console.error('❌ Request handler error:', error.message); + await say('Sorry, there was an error processing your request.'); + } + } + } catch (error) { + console.error('❌ Error in message handler:', error); + } + }); + + return app; +} + +async function updateBotStatus(app, status_text, status_emoji) { + try { + await app.client.users.profile.set({ + profile: { + status_text: status_text, + status_emoji: status_emoji + } + }); + console.log('✅ Bot status updated successfully'); + } catch (error) { + console.error('❌ Failed to update bot status:', error.message); + } +} + +module.exports = { + createSlackApp, + updateBotStatus +}; diff --git a/services/jellyfin-slackbot/src/validators.js b/services/jellyfin-slackbot/src/validators.js new file mode 100644 index 0000000..9ed978b --- /dev/null +++ b/services/jellyfin-slackbot/src/validators.js @@ -0,0 +1,48 @@ +const validateUrl = (url) => { + try { + new URL(url); + return true; + } catch { + return false; + } +}; + +const validateApiKey = (key) => { + return typeof key === 'string' && key.length > 0; +}; + +const requiredEnvVars = { + SLACK_BOT_TOKEN: (value) => value?.startsWith('xoxb-'), + SLACK_SIGNING_SECRET: (value) => typeof value === 'string' && value.length > 0, + JELLYSEERR_API_URL: validateUrl, + JELLYSEERR_API_KEY: validateApiKey +}; + +function validateEnvironment() { + const errors = []; + + for (const [varName, validator] of Object.entries(requiredEnvVars)) { + const value = process.env[varName]; + + if (!value) { + errors.push(`Missing ${varName}`); + continue; + } + + if (!validator(value)) { + errors.push(`Invalid ${varName}: ${value}`); + } + } + + if (errors.length > 0) { + throw new Error(`Environment validation failed:\n${errors.join('\n')}`); + } + + return true; +} + +module.exports = { + validateEnvironment, + validateUrl, + validateApiKey +}; \ No newline at end of file diff --git a/services/jellyfin-slackbot/tests/requestHandler.test.js b/services/jellyfin-slackbot/tests/requestHandler.test.js new file mode 100644 index 0000000..9500633 --- /dev/null +++ b/services/jellyfin-slackbot/tests/requestHandler.test.js @@ -0,0 +1,14 @@ +const requestHandler = require('../src/requestHandler'); + +describe('requestHandler', () => { + test('should handle successful video request', async () => { + const message = 'request video Test Video'; + const response = await requestHandler.handleRequest(message); + expect(response).toEqual({ success: true, message: 'Request forwarded to Jellyseerr' }); + }); + + test('should handle error in video request', async () => { + const message = 'request video Invalid Video'; + await expect(requestHandler.handleRequest(message)).rejects.toThrow('Failed to forward request to Jellyseerr'); + }); +}); diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..1a1d6c1 --- /dev/null +++ b/src/index.js @@ -0,0 +1,23 @@ +const { App } = require('@slack/bolt'); +const requestHandler = require('./requestHandler'); + +const app = new App({ + token: process.env.SLACK_BOT_TOKEN, + signingSecret: process.env.SLACK_SIGNING_SECRET +}); + +app.message(async ({ message, say }) => { + if (message.text.includes('request video')) { + try { + const response = await requestHandler.handleRequest(message.text); + await say(`Request received: ${response}`); + } catch (error) { + await say(`Error: ${error.message}`); + } + } +}); + +(async () => { + await app.start(process.env.PORT || 3000); + console.log('⚡️ Slack bot is running!'); +})(); diff --git a/src/requestHandler.js b/src/requestHandler.js new file mode 100644 index 0000000..f0ecc7c --- /dev/null +++ b/src/requestHandler.js @@ -0,0 +1,28 @@ +const axios = require('axios'); + +async function handleRequest(message) { + const videoRequest = parseMessage(message); + const response = await forwardToJellyseerr(videoRequest); + return response; +} + +function parseMessage(message) { + // Extract video request details from the message + // This is a placeholder implementation, adjust as needed + return { + title: message.split('request video ')[1] + }; +} + +async function forwardToJellyseerr(videoRequest) { + try { + const response = await axios.post(process.env.JELLYSEERR_API_URL, videoRequest); + return response.data; + } catch (error) { + throw new Error('Failed to forward request to Jellyseerr'); + } +} + +module.exports = { + handleRequest +}; diff --git a/tests/requestHandler.test.js b/tests/requestHandler.test.js new file mode 100644 index 0000000..9500633 --- /dev/null +++ b/tests/requestHandler.test.js @@ -0,0 +1,14 @@ +const requestHandler = require('../src/requestHandler'); + +describe('requestHandler', () => { + test('should handle successful video request', async () => { + const message = 'request video Test Video'; + const response = await requestHandler.handleRequest(message); + expect(response).toEqual({ success: true, message: 'Request forwarded to Jellyseerr' }); + }); + + test('should handle error in video request', async () => { + const message = 'request video Invalid Video'; + await expect(requestHandler.handleRequest(message)).rejects.toThrow('Failed to forward request to Jellyseerr'); + }); +}); From 62edfc7300bf465caf29faa8e66fef020a01ee88 Mon Sep 17 00:00:00 2001 From: Eric Wehrly <1851833+EricWehrly@users.noreply.github.com> Date: Sat, 1 Mar 2025 15:44:45 -0500 Subject: [PATCH 2/2] thank the dieties, something finally works --- .../jellyfin-slackbot/src/requestHandler.js | 71 +++++++++++++++---- 1 file changed, 59 insertions(+), 12 deletions(-) diff --git a/services/jellyfin-slackbot/src/requestHandler.js b/services/jellyfin-slackbot/src/requestHandler.js index b22c7cc..20c0b2e 100644 --- a/services/jellyfin-slackbot/src/requestHandler.js +++ b/services/jellyfin-slackbot/src/requestHandler.js @@ -1,25 +1,72 @@ const axios = require('axios'); async function handleRequest(message) { - const videoRequest = parseMessage(message); - const response = await forwardToJellyseerr(videoRequest); - return response; + console.log('📝 Parsing message:', message); + const searchQuery = parseMessage(message); + + // First search for the media + const searchResults = await searchMedia(searchQuery); + + if (!searchResults.length) { + return `No results found for "${searchQuery}"`; + } + + // Use the first result to submit a request + const requestResult = await submitMediaRequest(searchResults[0]); + return requestResult; } function parseMessage(message) { - // Extract video request details from the message - // This is a placeholder implementation, adjust as needed - return { - title: message.split('request video ')[1] || message - }; + // Extract the search query from message + // This regex matches "request video [title]" or similar patterns + const match = message.match(/request\s+(video|movie|tv|show)\s+(.+)/i); + return match ? match[2].trim() : message.trim(); +} + +async function searchMedia(query) { + console.log(`🔍 Searching for: "${query}"`); + try { + const response = await axios.get(`${process.env.JELLYSEERR_API_URL}/api/v1/search`, { + params: { query }, + headers: { 'X-Api-Key': process.env.JELLYSEERR_API_KEY } + }); + + console.log(`✅ Search found ${response.data.results.length} results`); + return response.data.results.slice(0, 3); // Return top 3 results + } catch (error) { + console.error('❌ Search error:', error.message); + if (error.response) { + console.error('Response data:', error.response.data); + } + return []; + } } -async function forwardToJellyseerr(videoRequest) { +async function submitMediaRequest(media) { + console.log(`🎬 Submitting request for: ${media.title || media.name}`); try { - const response = await axios.post(process.env.JELLYSEERR_API_URL, videoRequest); - return response.data; + // API reference: https://github.com/fallenbagel/jellyseerr/blob/main/overseerr-api.yml + const requestPayload = { + mediaId: media.id, + mediaType: media.mediaType, // "movie" or "tv" + // For TV shows, you might want to request all seasons: + // seasons: media.mediaType === 'tv' ? [1, 2, 3] : undefined + }; + + const response = await axios.post( + `${process.env.JELLYSEERR_API_URL}/api/v1/request`, + requestPayload, + { headers: { 'X-Api-Key': process.env.JELLYSEERR_API_KEY } } + ); + + console.log('✅ Request submitted successfully'); + return `Successfully requested ${media.title || media.name}!`; } catch (error) { - throw new Error('Failed to forward request to Jellyseerr'); + console.error('❌ Request submission error:', error.message); + if (error.response) { + console.error('Response data:', error.response.data); + } + throw new Error(`Failed to request: ${error.message}`); } }