From c8c0c1028866ebba4a42a24fa3163be205d73b61 Mon Sep 17 00:00:00 2001 From: Eric Wehrly <1851833+EricWehrly@users.noreply.github.com> Date: Mon, 24 Feb 2025 10:45:28 -0500 Subject: [PATCH] Add Jellyfin Slack bot --- For more details, open the [Copilot Workspace session](https://copilot-workspace.githubnext.com/EricWehrly/RaspberryiPi?shareId=XXXX-XXXX-XXXX-XXXX). --- .env.example | 4 + .github/workflows/ci.yml | 25 ++++++ Dockerfile | 11 +++ README.md | 84 +++++++++++++++++++ docker-compose.yml | 10 +++ jellyfin-slackbot/.env.example | 4 + jellyfin-slackbot/.github/workflows/ci.yml | 25 ++++++ jellyfin-slackbot/Dockerfile | 11 +++ jellyfin-slackbot/docker-compose.yml | 10 +++ jellyfin-slackbot/src/index.js | 23 +++++ jellyfin-slackbot/src/requestHandler.js | 28 +++++++ .../tests/requestHandler.test.js | 14 ++++ machines/pi/docker-compose.yml | 4 + src/index.js | 23 +++++ src/requestHandler.js | 28 +++++++ tests/requestHandler.test.js | 14 ++++ 16 files changed, 318 insertions(+) create mode 100644 .env.example create mode 100644 .github/workflows/ci.yml create mode 100644 Dockerfile create mode 100644 docker-compose.yml create mode 100644 jellyfin-slackbot/.env.example create mode 100644 jellyfin-slackbot/.github/workflows/ci.yml create mode 100644 jellyfin-slackbot/Dockerfile create mode 100644 jellyfin-slackbot/docker-compose.yml create mode 100644 jellyfin-slackbot/src/index.js create mode 100644 jellyfin-slackbot/src/requestHandler.js create mode 100644 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/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..999f4dd --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,25 @@ +name: CI + +on: + pull_request: + branches: + - main + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up Node.js + uses: actions/setup-node@v2 + with: + node-version: '14' + + - name: Install dependencies + run: npm install + + - name: Run tests + run: npm test 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/README.md b/README.md index 5478c4c..5a65d85 100644 --- a/README.md +++ b/README.md @@ -45,3 +45,87 @@ This repository is intended for operating a home server off of a Raspberry Pi. I ## Multi-Device Usage This repository is designed to be used across multiple devices, making it easier to migrate where and how the various services are hosted. The scripts and configurations are intended to be flexible and adaptable to different environments. + +--- + +## 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. + +### 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 + JELLYSEERR_API_URL=your-jellyseerr-api-url + ``` + +--- + +## 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} + - JELLYSEERR_API_URL=${JELLYSEERR_API_URL} + 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/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/jellyfin-slackbot/.env.example b/jellyfin-slackbot/.env.example new file mode 100644 index 0000000..4ed08ee --- /dev/null +++ b/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/jellyfin-slackbot/.github/workflows/ci.yml b/jellyfin-slackbot/.github/workflows/ci.yml new file mode 100644 index 0000000..999f4dd --- /dev/null +++ b/jellyfin-slackbot/.github/workflows/ci.yml @@ -0,0 +1,25 @@ +name: CI + +on: + pull_request: + branches: + - main + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up Node.js + uses: actions/setup-node@v2 + with: + node-version: '14' + + - name: Install dependencies + run: npm install + + - name: Run tests + run: npm test diff --git a/jellyfin-slackbot/Dockerfile b/jellyfin-slackbot/Dockerfile new file mode 100644 index 0000000..073b89b --- /dev/null +++ b/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/jellyfin-slackbot/docker-compose.yml b/jellyfin-slackbot/docker-compose.yml new file mode 100644 index 0000000..a1d1422 --- /dev/null +++ b/jellyfin-slackbot/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/jellyfin-slackbot/src/index.js b/jellyfin-slackbot/src/index.js new file mode 100644 index 0000000..1a1d6c1 --- /dev/null +++ b/jellyfin-slackbot/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/jellyfin-slackbot/src/requestHandler.js b/jellyfin-slackbot/src/requestHandler.js new file mode 100644 index 0000000..f0ecc7c --- /dev/null +++ b/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] + }; +} + +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/jellyfin-slackbot/tests/requestHandler.test.js b/jellyfin-slackbot/tests/requestHandler.test.js new file mode 100644 index 0000000..9500633 --- /dev/null +++ b/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/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/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'); + }); +});