From 144b925bca50b9ef5da7c143314ef204471706e0 Mon Sep 17 00:00:00 2001 From: retardgerman <78982850+retardgerman@users.noreply.github.com> Date: Tue, 10 Dec 2024 22:33:34 +0100 Subject: [PATCH 01/43] Create docker-image.yml --- .github/workflows/docker-image.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .github/workflows/docker-image.yml diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml new file mode 100644 index 0000000..3f53646 --- /dev/null +++ b/.github/workflows/docker-image.yml @@ -0,0 +1,18 @@ +name: Docker Image CI + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Build the Docker image + run: docker build . --file Dockerfile --tag my-image-name:$(date +%s) From 76752a6897df1aca6e4d5eb5a8fa5e9497e81e07 Mon Sep 17 00:00:00 2001 From: retardgerman <78982850+retardgerman@users.noreply.github.com> Date: Tue, 10 Dec 2024 22:42:55 +0100 Subject: [PATCH 02/43] Delete .github/workflows directory --- .github/workflows/docker-image.yml | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 .github/workflows/docker-image.yml diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml deleted file mode 100644 index 3f53646..0000000 --- a/.github/workflows/docker-image.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: Docker Image CI - -on: - push: - branches: [ "main" ] - pull_request: - branches: [ "main" ] - -jobs: - - build: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - name: Build the Docker image - run: docker build . --file Dockerfile --tag my-image-name:$(date +%s) From 4af82448d6e44ae823963c079059c896a2541772 Mon Sep 17 00:00:00 2001 From: retardgerman <78982850+retardgerman@users.noreply.github.com> Date: Wed, 11 Dec 2024 07:41:56 +0100 Subject: [PATCH 03/43] Create docker-publish.yml --- .github/workflows/docker-publish.yml | 31 ++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .github/workflows/docker-publish.yml diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 0000000..bf6680c --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -0,0 +1,31 @@ +name: Build and Push Docker Image + +on: + push: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + + steps: + # Checkout repository code + - name: Checkout code + uses: actions/checkout@v3 + + # Login to Docker Hub + - name: Log in to Docker Hub + run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin + + # Build the Docker image + - name: Build Docker image + run: docker build -t ${{ secrets.DOCKER_USERNAME }}/discord-bot:latest . + + # Push the Docker image to Docker Hub + - name: Push Docker image with version tag + run: | + IMAGE_VERSION=$(date +'%Y%m%d%H%M%S') + docker tag ${{ secrets.DOCKER_USERNAME }}/discord-bot:latest ${{ secrets.DOCKER_USERNAME }}/discord-bot:${IMAGE_VERSION} + docker push ${{ secrets.DOCKER_USERNAME }}/discord-bot:${IMAGE_VERSION} + From 28fc9397d885028587aeb4b528c0c0dc4c779c90 Mon Sep 17 00:00:00 2001 From: retardgerman <78982850+retardgerman@users.noreply.github.com> Date: Wed, 11 Dec 2024 07:51:05 +0100 Subject: [PATCH 04/43] recreate docker-publish.yml --- .github/workflows/docker-publish.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index bf6680c..c7747ae 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -14,18 +14,18 @@ jobs: - name: Checkout code uses: actions/checkout@v3 - # Login to Docker Hub + # Log in to Docker Hub - name: Log in to Docker Hub run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin - # Build the Docker image + # Build the Docker image with dynamic name - name: Build Docker image - run: docker build -t ${{ secrets.DOCKER_USERNAME }}/discord-bot:latest . + run: | + IMAGE_NAME="${{ secrets.DOCKER_USERNAME }}/${{ github.repository }}" + docker build -t $IMAGE_NAME:latest . # Push the Docker image to Docker Hub - - name: Push Docker image with version tag + - name: Push Docker image run: | - IMAGE_VERSION=$(date +'%Y%m%d%H%M%S') - docker tag ${{ secrets.DOCKER_USERNAME }}/discord-bot:latest ${{ secrets.DOCKER_USERNAME }}/discord-bot:${IMAGE_VERSION} - docker push ${{ secrets.DOCKER_USERNAME }}/discord-bot:${IMAGE_VERSION} - + IMAGE_NAME="${{ secrets.DOCKER_USERNAME }}/${{ github.repository }}" + docker push $IMAGE_NAME:latest \ No newline at end of file From 5a9a89eef85529aaf507d38207552c7d1d7f034a Mon Sep 17 00:00:00 2001 From: retardgerman <78982850+retardgerman@users.noreply.github.com> Date: Wed, 11 Dec 2024 08:00:50 +0100 Subject: [PATCH 05/43] new docker-publish.yml --- .github/workflows/docker-publish.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index c7747ae..657a3bc 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -18,14 +18,14 @@ jobs: - name: Log in to Docker Hub run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin - # Build the Docker image with dynamic name + # Build the Docker image - name: Build Docker image run: | - IMAGE_NAME="${{ secrets.DOCKER_USERNAME }}/${{ github.repository }}" + IMAGE_NAME="${{ secrets.DOCKER_USERNAME }}/streamyfin-discord" docker build -t $IMAGE_NAME:latest . # Push the Docker image to Docker Hub - name: Push Docker image run: | - IMAGE_NAME="${{ secrets.DOCKER_USERNAME }}/${{ github.repository }}" + IMAGE_NAME="${{ secrets.DOCKER_USERNAME }}/streamyfin-discord" docker push $IMAGE_NAME:latest \ No newline at end of file From 568b40ba688a0afa35e74a6586b8512b1884ac5c Mon Sep 17 00:00:00 2001 From: SimplyJanDE <78982850+SimplyJanDE@users.noreply.github.com> Date: Wed, 11 Dec 2024 18:59:57 +0100 Subject: [PATCH 06/43] Add createissue command to bot --- index.js | 230 +++++++++++++++++++++++++++++++++------------- package-lock.json | 100 ++++++++++++++++++++ package.json | 1 + 3 files changed, 269 insertions(+), 62 deletions(-) diff --git a/index.js b/index.js index 459e9dc..ff2d901 100644 --- a/index.js +++ b/index.js @@ -1,79 +1,185 @@ -const { Client, GatewayIntentBits, REST, Routes } = require('discord.js'); -require('dotenv').config(); +require("dotenv").config(); +const { Client, GatewayIntentBits, REST, Routes } = require("discord.js"); +const axios = require("axios"); -const GITHUB_REPO = 'https://github.com/fredrikburmester/streamyfin'; +// GitHub API-Basis-URL und Repo-Daten +const GITHUB_API_BASE = "https://api.github.com"; +const REPO_OWNER = "fredrikburmester"; +const REPO_NAME = "streamyfin"; +const GITHUB_TOKEN = process.env.GITHUB_TOKEN; -// Command Definitions +// Discord Client initialisieren +const client = new Client({ + intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages], +}); + +// Slash Commands registrieren const commands = [ - { - name: 'roadmap', - description: 'Sends the GitHub roadmap link.', - }, - { - name: 'issue', - description: 'Links to a specific GitHub issue. Usage: /issue ', - options: [ - { - name: 'number', - description: 'The issue number to link to.', - type: 4, // Integer type - required: true, - }, - ], - }, - { - name: 'testflight', - description: 'Explains how to join the Streamyfin testflight.', - }, + { + name: "roadmap", + description: "Get the link to the GitHub roadmap.", + }, + { + name: "issue", + description: "Get details about a specific issue from GitHub.", + options: [ + { + name: "number", + type: 4, // Integer + description: "The issue number", + required: true, + }, + ], + }, + { + name: "createissue", + description: "Create a new issue on GitHub.", + }, ]; -// Register Commands via Discord API -const rest = new REST({ version: '10' }).setToken(process.env.DISCORD_TOKEN); +const rest = new REST({ version: "10" }).setToken(process.env.DISCORD_TOKEN); +// Commands auf Discord hochladen (async () => { - try { - console.log('Registering slash commands...'); + try { + console.log("Registering slash commands..."); + await rest.put(Routes.applicationCommands(process.env.CLIENT_ID), { + body: commands, + }); + console.log("Slash commands registered successfully!"); + } catch (error) { + console.error("Error registering slash commands:", error); + } +})(); + +// Event: Bot bereit +client.once("ready", () => { + console.log(`Logged in as ${client.user.tag}!`); +}); + +// Slash Command: /roadmap +client.on("interactionCreate", async (interaction) => { + if (!interaction.isCommand()) return; + + const { commandName, options } = interaction; - await rest.put( - Routes.applicationCommands(process.env.CLIENT_ID), - { body: commands } - ); + if (commandName === "roadmap") { + await interaction.reply( + "πŸ“Œ Here is our Roadmap: https://github.com/fredrikburmester/streamyfin/projects/5" + ); + } - console.log('Slash commands registered successfully!'); + // Slash Command: /issue + if (commandName === "issue") { + const issueNumber = options.getInteger("number"); + + try { + const response = await axios.get( + `${GITHUB_API_BASE}/repos/${REPO_OWNER}/${REPO_NAME}/issues/${issueNumber}`, + { + headers: { + Authorization: `token ${GITHUB_TOKEN}`, + }, + } + ); + + const issue = response.data; + await interaction.reply( + `πŸ”— **Issue #${issue.number}: ${issue.title}**\n${issue.html_url}` + ); } catch (error) { - console.error('Failed to register slash commands:', error); + await interaction.reply("❌ Issue not found or an error occurred."); } -})(); + } -const client = new Client({ - intents: [GatewayIntentBits.Guilds], -}); + // Slash Command: /createissue + if (commandName === "createissue") { + const targetUser = interaction.user; -// Handle Command Execution -client.on('interactionCreate', async (interaction) => { - if (!interaction.isCommand()) return; - - const { commandName, options } = interaction; - - if (commandName === 'roadmap') { - await interaction.reply(`Here is the roadmap: ${GITHUB_REPO}/projects/5`); - } else if (commandName === 'issue') { - const issueNumber = options.getInteger('number'); - await interaction.reply( - `Here is the link to Issue #${issueNumber}: ${GITHUB_REPO}/issues/${issueNumber}` - ); - } else if (commandName === 'testflight') { - const userId = '398161771476549654'; - await interaction.reply( - 'Currently the only way to join our Beta is to reach out to <@${userId}> via DM and send him your email address.' - ); - } -}); + const questions = [ + "What happened? Also tell us, what did you expect to happen?", + "How do you trigger this bug? Please provide the reproduction steps step by step.", + "Which device and operating system are you using? (e.g., iPhone 15, iOS 18.1.1)", + "What version of Streamyfin are you running? (Options: 0.22.0, 0.21.0, older)", + "If applicable, please add screenshots to help explain your problem (paste links or describe).", + ]; -// Event: Bot Ready -client.once('ready', () => { - console.log(`Bot is online! Logged in as ${client.user.tag}`); + const answers = []; + + try { + const thread = await interaction.channel.threads.create({ + name: `Issue Creation: ${targetUser.username}`, + type: 11, + autoArchiveDuration: 60, + reason: "GitHub issue creation", + }); + + await thread.members.add(targetUser); + await thread.send( + `${targetUser}, let's create a GitHub issue! I'll ask you a few questions.` + ); + + for (const question of questions) { + await thread.send(question); + + const collected = await thread.awaitMessages({ + filter: (response) => response.author.id === targetUser.id, + max: 1, + time: 60000, + }); + + if (collected.size === 0) throw new Error("You didn't respond in time."); + answers.push(collected.first().content); + } + + const [whatHappened, reproSteps, device, version, screenshots] = answers; + + const body = ` +### What happened? +${whatHappened} + +### Reproduction steps +${reproSteps} + +### Device and operating system +${device} + +### Version +${version} + +### Screenshots +${screenshots} +`; + + const issueResponse = await axios.post( + `${GITHUB_API_BASE}/repos/${REPO_OWNER}/${REPO_NAME}/issues`, + { + title: `[Bug]: ${whatHappened.slice(0, 50)}`, + body: body, + labels: ["❌ bug"], + assignees: ["fredrikburmester"], + }, + { + headers: { + Authorization: `token ${GITHUB_TOKEN}`, + }, + } + ); + + await thread.send(`βœ… Issue created successfully: ${issueResponse.data.html_url}`); + await thread.send("This thread will automatically close shortly."); + + setTimeout(async () => { + await thread.setArchived(true); + }, 60000); + } catch (error) { + console.error(error); + await interaction.reply( + `${interaction.user}, I couldn't complete the issue creation process. Please try again.` + ); + } + } }); -// Login the Bot +// Bot starten client.login(process.env.DISCORD_TOKEN); diff --git a/package-lock.json b/package-lock.json index ab6f787..6bacff5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "axios": "^1.7.9", "discord.js": "^14.16.3", "dotenv": "^16.4.7" } @@ -225,6 +226,44 @@ "npm": ">=7.0.0" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.7.9", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", + "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/discord-api-types": { "version": "0.37.100", "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.100.tgz", @@ -275,6 +314,40 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -293,6 +366,33 @@ "integrity": "sha512-/k20Lg2q8LE5xiaaSkMXk4sfvI+9EGEykFS4b0CHHGWqDYU0bGUFSwchNOMA56D7TCs9GwVTkqe9als1/ns8UQ==", "license": "MIT" }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/ts-mixer": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz", diff --git a/package.json b/package.json index 431f969..5490921 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "license": "ISC", "description": "", "dependencies": { + "axios": "^1.7.9", "discord.js": "^14.16.3", "dotenv": "^16.4.7" } From 58015abb39bd4e20a7d0612c150ab2ed729b4caa Mon Sep 17 00:00:00 2001 From: SimplyJanDE <78982850+SimplyJanDE@users.noreply.github.com> Date: Wed, 11 Dec 2024 20:00:01 +0100 Subject: [PATCH 07/43] Add english code comments --- index.js | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/index.js b/index.js index ff2d901..ad1d6e4 100644 --- a/index.js +++ b/index.js @@ -2,18 +2,18 @@ require("dotenv").config(); const { Client, GatewayIntentBits, REST, Routes } = require("discord.js"); const axios = require("axios"); -// GitHub API-Basis-URL und Repo-Daten +// GitHub API base URL and repository data const GITHUB_API_BASE = "https://api.github.com"; const REPO_OWNER = "fredrikburmester"; const REPO_NAME = "streamyfin"; const GITHUB_TOKEN = process.env.GITHUB_TOKEN; -// Discord Client initialisieren +// Initialize the Discord Client const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages], }); -// Slash Commands registrieren +// Register slash commands const commands = [ { name: "roadmap", @@ -39,7 +39,7 @@ const commands = [ const rest = new REST({ version: "10" }).setToken(process.env.DISCORD_TOKEN); -// Commands auf Discord hochladen +// Upload slash commands to Discord (async () => { try { console.log("Registering slash commands..."); @@ -52,7 +52,7 @@ const rest = new REST({ version: "10" }).setToken(process.env.DISCORD_TOKEN); } })(); -// Event: Bot bereit +// Event: Bot is ready client.once("ready", () => { console.log(`Logged in as ${client.user.tag}!`); }); @@ -107,10 +107,11 @@ client.on("interactionCreate", async (interaction) => { const answers = []; try { + // Create a private thread for issue creation const thread = await interaction.channel.threads.create({ name: `Issue Creation: ${targetUser.username}`, - type: 11, - autoArchiveDuration: 60, + type: 11, // Private thread + autoArchiveDuration: 60, // Archive duration in minutes reason: "GitHub issue creation", }); @@ -119,6 +120,7 @@ client.on("interactionCreate", async (interaction) => { `${targetUser}, let's create a GitHub issue! I'll ask you a few questions.` ); + // Ask the user questions one by one for (const question of questions) { await thread.send(question); @@ -151,6 +153,7 @@ ${version} ${screenshots} `; + // Send the collected data to GitHub to create a new issue const issueResponse = await axios.post( `${GITHUB_API_BASE}/repos/${REPO_OWNER}/${REPO_NAME}/issues`, { @@ -169,6 +172,7 @@ ${screenshots} await thread.send(`βœ… Issue created successfully: ${issueResponse.data.html_url}`); await thread.send("This thread will automatically close shortly."); + // Automatically archive the thread after 1 minute setTimeout(async () => { await thread.setArchived(true); }, 60000); @@ -181,5 +185,5 @@ ${screenshots} } }); -// Bot starten +// Start the bot client.login(process.env.DISCORD_TOKEN); From d056341fd878c3d96cb8c3120e799ecefababf23 Mon Sep 17 00:00:00 2001 From: SimplyJanDE <78982850+SimplyJanDE@users.noreply.github.com> Date: Wed, 11 Dec 2024 20:03:04 +0100 Subject: [PATCH 08/43] updated env example --- .env.example | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index 5df1ce1..cff6dc0 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,3 @@ -DISCORD_TOKEN=YOUR_DISCORD_BOT_TOKEN \ No newline at end of file +DISCORD_TOKEN=your_discord_bot_token +CLIENT_ID=your_discord_bot_client_id +GITHUB_TOKEN=your_github_token \ No newline at end of file From 63911a518d18682a33442dbacaafaf10c51a8d99 Mon Sep 17 00:00:00 2001 From: SimplyJanDE <78982850+SimplyJanDE@users.noreply.github.com> Date: Thu, 12 Dec 2024 13:54:20 +0100 Subject: [PATCH 09/43] new command handling --- index.js | 208 +++++++++++++++++++++++++++++++------------------------ 1 file changed, 117 insertions(+), 91 deletions(-) diff --git a/index.js b/index.js index ad1d6e4..8cc0359 100644 --- a/index.js +++ b/index.js @@ -2,45 +2,113 @@ require("dotenv").config(); const { Client, GatewayIntentBits, REST, Routes } = require("discord.js"); const axios = require("axios"); -// GitHub API base URL and repository data +// GitHub API base URL and repo data const GITHUB_API_BASE = "https://api.github.com"; const REPO_OWNER = "fredrikburmester"; const REPO_NAME = "streamyfin"; const GITHUB_TOKEN = process.env.GITHUB_TOKEN; -// Initialize the Discord Client +// Function to fetch releases from GitHub +const fetchReleases = async () => { + try { + const response = await axios.get( + `${GITHUB_API_BASE}/repos/${REPO_OWNER}/${REPO_NAME}/releases`, + { + headers: { + Authorization: `token ${GITHUB_TOKEN}`, + }, + } + ); + + const releases = response.data + .slice(0, 2) // Get the latest 2 releases + .map((release) => ({ name: release.name, value: release.name })); + + releases.push({ name: "Older", value: "Older" }); // Add "Older" as an option + + return releases; + } catch (error) { + console.error("Error fetching releases from GitHub:", error); + return [ + { name: "0.22.0", value: "0.22.0" }, // Fallback data + { name: "0.21.0", value: "0.21.0" }, + { name: "Older", value: "Older" }, + ]; + } +}; + +// Initialize Discord client const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages], }); -// Register slash commands -const commands = [ - { - name: "roadmap", - description: "Get the link to the GitHub roadmap.", - }, - { - name: "issue", - description: "Get details about a specific issue from GitHub.", - options: [ - { - name: "number", - type: 4, // Integer - description: "The issue number", - required: true, - }, - ], - }, - { - name: "createissue", - description: "Create a new issue on GitHub.", - }, -]; - -const rest = new REST({ version: "10" }).setToken(process.env.DISCORD_TOKEN); - -// Upload slash commands to Discord -(async () => { +const registerCommands = async () => { + const releaseChoices = await fetchReleases(); + + const commands = [ + { + name: "roadmap", + description: "Get the link to the GitHub roadmap.", + }, + { + name: "issue", + description: "Get details about a specific issue from GitHub.", + options: [ + { + name: "number", + type: 4, // Integer + description: "The issue number", + required: true, + }, + ], + }, + { + name: "createissue", + description: "Create a new issue on GitHub.", + options: [ + { + name: "title", + type: 3, // String + description: "Short title describing the issue", + required: true, + }, + { + name: "description", + type: 3, // String + description: "What happened? What did you expect to happen?", + required: true, + }, + { + name: "steps", + type: 3, // String + description: "How can this issue be reproduced? (Step-by-step)", + required: true, + }, + { + name: "device", + type: 3, // String + description: "Device and operating system (e.g., iPhone 15, iOS 18.1.1)", + required: true, + }, + { + name: "version", + type: 3, // String + description: "Streamyfin version", + required: true, + choices: releaseChoices, + }, + { + name: "screenshots", + type: 3, // String + description: "Links to screenshots (if applicable)", + required: false, + }, + ], + }, + ]; + + const rest = new REST({ version: "10" }).setToken(process.env.DISCORD_TOKEN); + try { console.log("Registering slash commands..."); await rest.put(Routes.applicationCommands(process.env.CLIENT_ID), { @@ -50,14 +118,13 @@ const rest = new REST({ version: "10" }).setToken(process.env.DISCORD_TOKEN); } catch (error) { console.error("Error registering slash commands:", error); } -})(); +}; -// Event: Bot is ready client.once("ready", () => { console.log(`Logged in as ${client.user.tag}!`); }); -// Slash Command: /roadmap +// Handle slash command interactions client.on("interactionCreate", async (interaction) => { if (!interaction.isCommand()) return; @@ -69,7 +136,6 @@ client.on("interactionCreate", async (interaction) => { ); } - // Slash Command: /issue if (commandName === "issue") { const issueNumber = options.getInteger("number"); @@ -92,56 +158,22 @@ client.on("interactionCreate", async (interaction) => { } } - // Slash Command: /createissue if (commandName === "createissue") { - const targetUser = interaction.user; - - const questions = [ - "What happened? Also tell us, what did you expect to happen?", - "How do you trigger this bug? Please provide the reproduction steps step by step.", - "Which device and operating system are you using? (e.g., iPhone 15, iOS 18.1.1)", - "What version of Streamyfin are you running? (Options: 0.22.0, 0.21.0, older)", - "If applicable, please add screenshots to help explain your problem (paste links or describe).", - ]; + const title = options.getString("title"); + const description = options.getString("description"); + const steps = options.getString("steps"); + const device = options.getString("device"); + const version = options.getString("version"); + const screenshots = options.getString("screenshots") || "No screenshots provided"; - const answers = []; + const username = interaction.user.username; - try { - // Create a private thread for issue creation - const thread = await interaction.channel.threads.create({ - name: `Issue Creation: ${targetUser.username}`, - type: 11, // Private thread - autoArchiveDuration: 60, // Archive duration in minutes - reason: "GitHub issue creation", - }); - - await thread.members.add(targetUser); - await thread.send( - `${targetUser}, let's create a GitHub issue! I'll ask you a few questions.` - ); - - // Ask the user questions one by one - for (const question of questions) { - await thread.send(question); - - const collected = await thread.awaitMessages({ - filter: (response) => response.author.id === targetUser.id, - max: 1, - time: 60000, - }); - - if (collected.size === 0) throw new Error("You didn't respond in time."); - answers.push(collected.first().content); - } - - const [whatHappened, reproSteps, device, version, screenshots] = answers; - - const body = ` + const body = ` ### What happened? -${whatHappened} +${description} ### Reproduction steps -${reproSteps} +${steps} ### Device and operating system ${device} @@ -153,11 +185,11 @@ ${version} ${screenshots} `; - // Send the collected data to GitHub to create a new issue + try { const issueResponse = await axios.post( `${GITHUB_API_BASE}/repos/${REPO_OWNER}/${REPO_NAME}/issues`, { - title: `[Bug]: ${whatHappened.slice(0, 50)}`, + title: `[Bug][${username}]: ${title}`, body: body, labels: ["❌ bug"], assignees: ["fredrikburmester"], @@ -169,21 +201,15 @@ ${screenshots} } ); - await thread.send(`βœ… Issue created successfully: ${issueResponse.data.html_url}`); - await thread.send("This thread will automatically close shortly."); - - // Automatically archive the thread after 1 minute - setTimeout(async () => { - await thread.setArchived(true); - }, 60000); - } catch (error) { - console.error(error); await interaction.reply( - `${interaction.user}, I couldn't complete the issue creation process. Please try again.` + `βœ… Issue created successfully: ${issueResponse.data.html_url}` ); + } catch (error) { + console.error("Error creating issue:", error); + await interaction.reply("❌ Failed to create the issue. Please try again."); } } }); -// Start the bot +registerCommands(); client.login(process.env.DISCORD_TOKEN); From abccdd95c7dd3cf25db25d958735b1d804e36223 Mon Sep 17 00:00:00 2001 From: SimplyJanDE <78982850+SimplyJanDE@users.noreply.github.com> Date: Thu, 12 Dec 2024 18:37:13 +0100 Subject: [PATCH 10/43] add new help command --- index.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/index.js b/index.js index 8cc0359..6e43747 100644 --- a/index.js +++ b/index.js @@ -105,6 +105,10 @@ const registerCommands = async () => { }, ], }, + { + name: "help", + description: "Get a list of available commands.", + }, ]; const rest = new REST({ version: "10" }).setToken(process.env.DISCORD_TOKEN); @@ -136,6 +140,17 @@ client.on("interactionCreate", async (interaction) => { ); } + if (commandName === "help") { + const commandList = commands + .map((cmd) => `**/${cmd.name}**: ${cmd.description}`) + .join("\n"); + + await interaction.reply({ + content: `Available commands:\n${commandList}`, + ephemeral: true, + }); + } + if (commandName === "issue") { const issueNumber = options.getInteger("number"); From c263b0bf6c865a7175d44f90240452b0dd4caf37 Mon Sep 17 00:00:00 2001 From: SimplyJanDE <78982850+SimplyJanDE@users.noreply.github.com> Date: Fri, 13 Dec 2024 13:56:09 +0100 Subject: [PATCH 11/43] add help command --- index.js | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/index.js b/index.js index 6e43747..2d8b796 100644 --- a/index.js +++ b/index.js @@ -8,7 +8,9 @@ const REPO_OWNER = "fredrikburmester"; const REPO_NAME = "streamyfin"; const GITHUB_TOKEN = process.env.GITHUB_TOKEN; -// Function to fetch releases from GitHub +let commands = []; // Globale Variable fΓΌr die Commands + +// Funktion zum Abrufen von Releases von GitHub const fetchReleases = async () => { try { const response = await axios.get( @@ -21,31 +23,32 @@ const fetchReleases = async () => { ); const releases = response.data - .slice(0, 2) // Get the latest 2 releases + .slice(0, 2) // Hol die letzten 2 Releases .map((release) => ({ name: release.name, value: release.name })); - releases.push({ name: "Older", value: "Older" }); // Add "Older" as an option + releases.push({ name: "Older", value: "Older" }); // FΓΌge "Older" als Option hinzu return releases; } catch (error) { - console.error("Error fetching releases from GitHub:", error); + console.error("Fehler beim Abrufen von Releases:", error); return [ - { name: "0.22.0", value: "0.22.0" }, // Fallback data + { name: "0.22.0", value: "0.22.0" }, // Fallback-Daten { name: "0.21.0", value: "0.21.0" }, { name: "Older", value: "Older" }, ]; } }; -// Initialize Discord client +// Discord-Client initialisieren const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages], }); +// Slash-Commands registrieren const registerCommands = async () => { const releaseChoices = await fetchReleases(); - const commands = [ + commands = [ // Globale Variable hier befΓΌllen { name: "roadmap", description: "Get the link to the GitHub roadmap.", @@ -118,17 +121,18 @@ const registerCommands = async () => { await rest.put(Routes.applicationCommands(process.env.CLIENT_ID), { body: commands, }); - console.log("Slash commands registered successfully!"); + console.log("Slash commands registered erfolgreich!"); } catch (error) { - console.error("Error registering slash commands:", error); + console.error("Fehler beim Registrieren der Slash-Commands:", error); } }; +// Event, wenn der Bot bereit ist client.once("ready", () => { console.log(`Logged in as ${client.user.tag}!`); }); -// Handle slash command interactions +// Slash-Command-Interaktionen verarbeiten client.on("interactionCreate", async (interaction) => { if (!interaction.isCommand()) return; @@ -226,5 +230,6 @@ ${screenshots} } }); +// Commands registrieren und Bot starten registerCommands(); client.login(process.env.DISCORD_TOKEN); From d8b4bcaf9efa166979022d2ad6d2e53c2aa10efe Mon Sep 17 00:00:00 2001 From: SimplyJanDE <78982850+SimplyJanDE@users.noreply.github.com> Date: Fri, 13 Dec 2024 17:32:11 +0100 Subject: [PATCH 12/43] add new createissue command --- index.js | 182 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 94 insertions(+), 88 deletions(-) diff --git a/index.js b/index.js index 2d8b796..8aa56b9 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,5 @@ require("dotenv").config(); -const { Client, GatewayIntentBits, REST, Routes } = require("discord.js"); +const { Client, GatewayIntentBits, REST, Routes, ChannelType, MessageCollector } = require("discord.js"); const axios = require("axios"); // GitHub API base URL and repo data @@ -8,9 +8,9 @@ const REPO_OWNER = "fredrikburmester"; const REPO_NAME = "streamyfin"; const GITHUB_TOKEN = process.env.GITHUB_TOKEN; -let commands = []; // Globale Variable fΓΌr die Commands +let commands = []; // Global commands array -// Funktion zum Abrufen von Releases von GitHub +// Function to fetch releases from GitHub const fetchReleases = async () => { try { const response = await axios.get( @@ -23,32 +23,32 @@ const fetchReleases = async () => { ); const releases = response.data - .slice(0, 2) // Hol die letzten 2 Releases + .slice(0, 2) // Fetch the latest 2 releases .map((release) => ({ name: release.name, value: release.name })); - releases.push({ name: "Older", value: "Older" }); // FΓΌge "Older" als Option hinzu + releases.push({ name: "Older", value: "Older" }); // Add "Older" as an option return releases; } catch (error) { - console.error("Fehler beim Abrufen von Releases:", error); + console.error("Error fetching releases:", error); return [ - { name: "0.22.0", value: "0.22.0" }, // Fallback-Daten + { name: "0.22.0", value: "0.22.0" }, // Fallback data { name: "0.21.0", value: "0.21.0" }, { name: "Older", value: "Older" }, ]; } }; -// Discord-Client initialisieren +// Initialize Discord client const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages], }); -// Slash-Commands registrieren +// Register slash commands const registerCommands = async () => { const releaseChoices = await fetchReleases(); - commands = [ // Globale Variable hier befΓΌllen + commands = [ { name: "roadmap", description: "Get the link to the GitHub roadmap.", @@ -68,45 +68,6 @@ const registerCommands = async () => { { name: "createissue", description: "Create a new issue on GitHub.", - options: [ - { - name: "title", - type: 3, // String - description: "Short title describing the issue", - required: true, - }, - { - name: "description", - type: 3, // String - description: "What happened? What did you expect to happen?", - required: true, - }, - { - name: "steps", - type: 3, // String - description: "How can this issue be reproduced? (Step-by-step)", - required: true, - }, - { - name: "device", - type: 3, // String - description: "Device and operating system (e.g., iPhone 15, iOS 18.1.1)", - required: true, - }, - { - name: "version", - type: 3, // String - description: "Streamyfin version", - required: true, - choices: releaseChoices, - }, - { - name: "screenshots", - type: 3, // String - description: "Links to screenshots (if applicable)", - required: false, - }, - ], }, { name: "help", @@ -121,18 +82,18 @@ const registerCommands = async () => { await rest.put(Routes.applicationCommands(process.env.CLIENT_ID), { body: commands, }); - console.log("Slash commands registered erfolgreich!"); + console.log("Slash commands registered successfully!"); } catch (error) { - console.error("Fehler beim Registrieren der Slash-Commands:", error); + console.error("Error registering slash commands:", error); } }; -// Event, wenn der Bot bereit ist +// Event when the bot is ready client.once("ready", () => { console.log(`Logged in as ${client.user.tag}!`); }); -// Slash-Command-Interaktionen verarbeiten +// Handle slash command interactions client.on("interactionCreate", async (interaction) => { if (!interaction.isCommand()) return; @@ -178,58 +139,103 @@ client.on("interactionCreate", async (interaction) => { } if (commandName === "createissue") { - const title = options.getString("title"); - const description = options.getString("description"); - const steps = options.getString("steps"); - const device = options.getString("device"); - const version = options.getString("version"); - const screenshots = options.getString("screenshots") || "No screenshots provided"; + // Start by creating a private thread + const thread = await interaction.channel.threads.create({ + name: `Issue Report by ${interaction.user.username}`, + autoArchiveDuration: 60, // Auto-archive after 1 hour + type: ChannelType.PrivateThread, + reason: "Collecting issue details", + }); + + await thread.members.add(interaction.user.id); // Add the user to the thread + await interaction.reply({ content: `βœ… Private thread created: ${thread.name}`, ephemeral: true }); + + // Define the questions to ask the user + const questions = [ + { key: "title", question: "What is the title of the issue?" }, + { key: "description", question: "What happened? What did you expect to happen?" }, + { key: "steps", question: "How can this issue be reproduced? (step-by-step)" }, + { key: "device", question: "What device and operating system are you using?" }, + { key: "version", question: "What is the affected Streamyfin version?" }, + { key: "screenshots", question: "Provide links to screenshots (if any), or type 'none'." }, + ]; - const username = interaction.user.username; + const collectedData = {}; // Store user responses here - const body = ` + // Helper function to ask questions + const askQuestion = async (questionIndex = 0) => { + if (questionIndex >= questions.length) { + // All questions answered, proceed to create the issue + const body = ` ### What happened? -${description} +${collectedData.description} ### Reproduction steps -${steps} +${collectedData.steps} ### Device and operating system -${device} +${collectedData.device} ### Version -${version} +${collectedData.version} ### Screenshots -${screenshots} +${collectedData.screenshots || "No screenshots provided"} `; - try { - const issueResponse = await axios.post( - `${GITHUB_API_BASE}/repos/${REPO_OWNER}/${REPO_NAME}/issues`, - { - title: `[Bug][${username}]: ${title}`, - body: body, - labels: ["❌ bug"], - assignees: ["fredrikburmester"], - }, - { - headers: { - Authorization: `token ${GITHUB_TOKEN}`, - }, + try { + const issueResponse = await axios.post( + `${GITHUB_API_BASE}/repos/${REPO_OWNER}/${REPO_NAME}/issues`, + { + title: `[Bug][${interaction.user.username}]: ${collectedData.title}`, + body: body, + labels: ["❌ bug"], + assignees: ["fredrikburmester"], + }, + { + headers: { + Authorization: `token ${GITHUB_TOKEN}`, + }, + } + ); + + await thread.send(`βœ… Issue created successfully: ${issueResponse.data.html_url}`); + await thread.setLocked(true, "Issue details collected and sent to GitHub."); + } catch (error) { + console.error("Error creating issue:", error); + await thread.send("❌ Failed to create the issue. Please try again."); } - ); + return; + } - await interaction.reply( - `βœ… Issue created successfully: ${issueResponse.data.html_url}` - ); - } catch (error) { - console.error("Error creating issue:", error); - await interaction.reply("❌ Failed to create the issue. Please try again."); - } + // Ask the next question + const question = questions[questionIndex]; + await thread.send(question.question); + + // Set up a message collector to get the user's response + const collector = new MessageCollector(thread, { + filter: (msg) => msg.author.id === interaction.user.id, + max: 1, // Collect only one message + time: 300000, // Timeout after 5 minutes + }); + + collector.on("collect", (msg) => { + collectedData[question.key] = msg.content; + askQuestion(questionIndex + 1); // Ask the next question + }); + + collector.on("end", (collected, reason) => { + if (reason === "time") { + thread.send("❌ You did not respond in time. Please run the command again if you'd like to create an issue."); + } + }); + }; + + // Start asking questions + askQuestion(); } }); -// Commands registrieren und Bot starten +// Register commands and start the bot registerCommands(); client.login(process.env.DISCORD_TOKEN); From b8838252a829dd441bd17196517ab4a98bcdde99 Mon Sep 17 00:00:00 2001 From: SimplyJanDE <78982850+SimplyJanDE@users.noreply.github.com> Date: Sat, 14 Dec 2024 15:21:38 +0100 Subject: [PATCH 13/43] add new commands --- docker-compose.yml | 17 +++++++++++++++++ index.js | 27 ++++++++++++++++++++++++--- 2 files changed, 41 insertions(+), 3 deletions(-) create mode 100644 docker-compose.yml diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..0167e97 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,17 @@ +version: "3.9" + +services: + discord-bot: + image: node:18-alpine + container_name: discord-bot + working_dir: /app + volumes: + - .:/app + environment: + - DISCORD_TOKEN=${DISCORD_TOKEN} # Discord bot token + - CLIENT_ID=${CLIENT_ID} # Discord bot client ID + - GITHUB_TOKEN=${GITHUB_TOKEN} # GitHub token for API requests + - REPO_OWNER=${REPO_OWNER} # GitHub repository owner + - REPO_NAME=${REPO_NAME} # GitHub repository name + command: sh -c "npm install && node index.js" # Install dependencies and run the bot + restart: unless-stopped # Restart the container if it crashes diff --git a/index.js b/index.js index 8aa56b9..445f144 100644 --- a/index.js +++ b/index.js @@ -4,8 +4,8 @@ const axios = require("axios"); // GitHub API base URL and repo data const GITHUB_API_BASE = "https://api.github.com"; -const REPO_OWNER = "fredrikburmester"; -const REPO_NAME = "streamyfin"; +const REPO_OWNER = process.env.REPO_OWNER; +const REPO_NAME = process.env.REPO_NAME; const GITHUB_TOKEN = process.env.GITHUB_TOKEN; let commands = []; // Global commands array @@ -41,7 +41,7 @@ const fetchReleases = async () => { // Initialize Discord client const client = new Client({ - intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages], + intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent], }); // Register slash commands @@ -73,6 +73,14 @@ const registerCommands = async () => { name: "help", description: "Get a list of available commands.", }, + { + name: "testflight", + description: "Explains how to join the Streamyfin Testflight.", + }, + { + name: "repo", + description: "Get the link to the GitHub repository." + }, ]; const rest = new REST({ version: "10" }).setToken(process.env.DISCORD_TOKEN); @@ -99,6 +107,12 @@ client.on("interactionCreate", async (interaction) => { const { commandName, options } = interaction; + if (commandName === "repo") { + await interaction.reply( + "πŸ“‘ Here is our GitHub repository: https://github.com/fredrikburmester/streamyfin" + ); + } + if (commandName === "roadmap") { await interaction.reply( "πŸ“Œ Here is our Roadmap: https://github.com/fredrikburmester/streamyfin/projects/5" @@ -138,6 +152,13 @@ client.on("interactionCreate", async (interaction) => { } } + if (commandName === "testflight") { + const userId = '398161771476549654'; + await interaction.reply( + `Currently, Streamyfin Testflight is full. However, you can send a private message to <@${userId}> with your email address and he will add you to the Testflight beta group manually.` + ); + } + if (commandName === "createissue") { // Start by creating a private thread const thread = await interaction.channel.threads.create({ From ca5bd0a6a3f2ebb785453dd1ee900d0e9813807f Mon Sep 17 00:00:00 2001 From: SimplyJanDE <78982850+SimplyJanDE@users.noreply.github.com> Date: Mon, 16 Dec 2024 12:28:14 +0100 Subject: [PATCH 14/43] createissue now posts in forum channel --- index.js | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/index.js b/index.js index 445f144..6a1fc40 100644 --- a/index.js +++ b/index.js @@ -160,16 +160,32 @@ client.on("interactionCreate", async (interaction) => { } if (commandName === "createissue") { - // Start by creating a private thread - const thread = await interaction.channel.threads.create({ + // Start by creating a private thread in forum + const forumChannelId= process.env.FORUM_CHANNEL_ID; + const forumChannel = interaction.guild.channels.cache.get(forumChannelId); + + if (!forumChannel || forumChannel.type !== ChannelType.GuildForum) { + await interaction.reply({ + content: "❌ Forum channel not found or is not a forum channel.", + ephemeral: true, + }); + return; + } + + const thread = await forumChannel.threads.create({ name: `Issue Report by ${interaction.user.username}`, + message: { + content: `Hello ${interaction.user.username}, let's collect the details for your issue report!`, + }, autoArchiveDuration: 60, // Auto-archive after 1 hour type: ChannelType.PrivateThread, reason: "Collecting issue details", }); - await thread.members.add(interaction.user.id); // Add the user to the thread - await interaction.reply({ content: `βœ… Private thread created: ${thread.name}`, ephemeral: true }); + await interaction.reply({ + content: `βœ… Forum thread created: [${thread.name}](https://discord.com/channels/${interaction.guild.id}/${forumChannelId}/${thread.id})`, + ephemeral: true, + }); // Define the questions to ask the user const questions = [ From 8bc716d2924d686275a1bdaa75bd52c19b2f28d7 Mon Sep 17 00:00:00 2001 From: SimplyJanDE <78982850+SimplyJanDE@users.noreply.github.com> Date: Mon, 16 Dec 2024 13:09:32 +0100 Subject: [PATCH 15/43] feat:screenshot support for createissue command --- index.js | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/index.js b/index.js index 6a1fc40..1059237 100644 --- a/index.js +++ b/index.js @@ -183,26 +183,30 @@ client.on("interactionCreate", async (interaction) => { }); await interaction.reply({ - content: `βœ… Forum thread created: [${thread.name}](https://discord.com/channels/${interaction.guild.id}/${forumChannelId}/${thread.id})`, + content: `βœ… Forum thread created, please fill out the issue report: [${thread.name}](https://discord.com/channels/${interaction.guild.id}/${forumChannelId}/${thread.id})`, ephemeral: true, }); // Define the questions to ask the user const questions = [ - { key: "title", question: "What is the title of the issue?" }, + { key: "title", question: "Describe your issue in a few words." }, { key: "description", question: "What happened? What did you expect to happen?" }, { key: "steps", question: "How can this issue be reproduced? (step-by-step)" }, { key: "device", question: "What device and operating system are you using?" }, { key: "version", question: "What is the affected Streamyfin version?" }, - { key: "screenshots", question: "Provide links to screenshots (if any), or type 'none'." }, + { key: "screenshots", question: "Please provide any screenshots that might help us reproduce the issue (optional), or type 'none'.", allowUploads: true }, ]; const collectedData = {}; // Store user responses here - + const uploadedFiles = []; // Store uploaded files here // Helper function to ask questions const askQuestion = async (questionIndex = 0) => { if (questionIndex >= questions.length) { - // All questions answered, proceed to create the issue + // All questions have been asked + const screenshotsText = uploadedFiles.length + ? uploadedFiles.map((file) => file.url).join("\n") + : "No screenshots provided"; + const body = ` ### What happened? ${collectedData.description} @@ -217,14 +221,14 @@ ${collectedData.device} ${collectedData.version} ### Screenshots -${collectedData.screenshots || "No screenshots provided"} +${screenshotsText} `; try { const issueResponse = await axios.post( `${GITHUB_API_BASE}/repos/${REPO_OWNER}/${REPO_NAME}/issues`, { - title: `[Bug][${interaction.user.username}]: ${collectedData.title}`, + title: `[Bug]: ${collectedData.title} reported via Discord by [${interaction.user.username}]`, body: body, labels: ["❌ bug"], assignees: ["fredrikburmester"], @@ -256,9 +260,19 @@ ${collectedData.screenshots || "No screenshots provided"} time: 300000, // Timeout after 5 minutes }); - collector.on("collect", (msg) => { - collectedData[question.key] = msg.content; - askQuestion(questionIndex + 1); // Ask the next question + collector.on("collect", async (msg) => { + if (question.allowUploads && msg.attachments.size > 0) { + msg.attachments.forEach((attachment) => { + uploadedFiles.push({ + name: attachment.filename, + url: attachment.url, + }); + }); + collectedData[question.key] = "Screenshots uploaded"; + } else { + collectedData[question.key] = msg.content; + } + askQuestion(questionIndex + 1); }); collector.on("end", (collected, reason) => { From 66f69d8ea0c90fcb7aa35f69fc2354c87044dbe3 Mon Sep 17 00:00:00 2001 From: SimplyJanDE <78982850+SimplyJanDE@users.noreply.github.com> Date: Mon, 16 Dec 2024 13:11:10 +0100 Subject: [PATCH 16/43] env update --- .env.example | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index cff6dc0..f88993e 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,4 @@ DISCORD_TOKEN=your_discord_bot_token CLIENT_ID=your_discord_bot_client_id -GITHUB_TOKEN=your_github_token \ No newline at end of file +GITHUB_TOKEN=your_github_token +FORUM_CHANNEL_ID=your_forum_channel_id \ No newline at end of file From b02050ec0b7a3fd1879680f9bc47877589b815be Mon Sep 17 00:00:00 2001 From: SimplyJanDE <78982850+SimplyJanDE@users.noreply.github.com> Date: Mon, 16 Dec 2024 13:15:20 +0100 Subject: [PATCH 17/43] docker compose update --- docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose.yml b/docker-compose.yml index 0167e97..387a1f1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,5 +13,6 @@ services: - GITHUB_TOKEN=${GITHUB_TOKEN} # GitHub token for API requests - REPO_OWNER=${REPO_OWNER} # GitHub repository owner - REPO_NAME=${REPO_NAME} # GitHub repository name + - FORUM_CHANNEL_ID=${FORUM_CHANNEL_ID} command: sh -c "npm install && node index.js" # Install dependencies and run the bot restart: unless-stopped # Restart the container if it crashes From 39e393cb8a7100ca950563013aa628833a079d79 Mon Sep 17 00:00:00 2001 From: SimplyJanDE <78982850+SimplyJanDE@users.noreply.github.com> Date: Wed, 18 Dec 2024 13:57:19 +0100 Subject: [PATCH 18/43] delete github workflows --- .github/workflows/docker-publish.yml | 31 ---------------------------- 1 file changed, 31 deletions(-) delete mode 100644 .github/workflows/docker-publish.yml diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml deleted file mode 100644 index 657a3bc..0000000 --- a/.github/workflows/docker-publish.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: Build and Push Docker Image - -on: - push: - branches: - - main - -jobs: - build: - runs-on: ubuntu-latest - - steps: - # Checkout repository code - - name: Checkout code - uses: actions/checkout@v3 - - # Log in to Docker Hub - - name: Log in to Docker Hub - run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin - - # Build the Docker image - - name: Build Docker image - run: | - IMAGE_NAME="${{ secrets.DOCKER_USERNAME }}/streamyfin-discord" - docker build -t $IMAGE_NAME:latest . - - # Push the Docker image to Docker Hub - - name: Push Docker image - run: | - IMAGE_NAME="${{ secrets.DOCKER_USERNAME }}/streamyfin-discord" - docker push $IMAGE_NAME:latest \ No newline at end of file From 8bdf1df06a2d6da695606e0be400359280a33474 Mon Sep 17 00:00:00 2001 From: retardgerman <78982850+retardgerman@users.noreply.github.com> Date: Sat, 21 Dec 2024 11:22:23 +0100 Subject: [PATCH 19/43] fix; correct link to project roadmap --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index 1059237..071fd7a 100644 --- a/index.js +++ b/index.js @@ -115,7 +115,7 @@ client.on("interactionCreate", async (interaction) => { if (commandName === "roadmap") { await interaction.reply( - "πŸ“Œ Here is our Roadmap: https://github.com/fredrikburmester/streamyfin/projects/5" + "πŸ“Œ Here is our Roadmap: https://github.com/fredrikburmester/streamyfin/projects/5/" ); } From c7b3983a72a714c9a2fb2352c679534d5d695ba6 Mon Sep 17 00:00:00 2001 From: SimplyJanDE <78982850+SimplyJanDE@users.noreply.github.com> Date: Sat, 21 Dec 2024 13:00:49 +0100 Subject: [PATCH 20/43] fix; roadmap link --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index 071fd7a..9880751 100644 --- a/index.js +++ b/index.js @@ -115,7 +115,7 @@ client.on("interactionCreate", async (interaction) => { if (commandName === "roadmap") { await interaction.reply( - "πŸ“Œ Here is our Roadmap: https://github.com/fredrikburmester/streamyfin/projects/5/" + "πŸ“Œ Here is our Roadmap: https://github.com/users/fredrikburmester/projects/5/views/8" ); } From 43f1f49ce526d3638dec2a740bd5b7af48e5ab10 Mon Sep 17 00:00:00 2001 From: SimplyJanDE <78982850+SimplyJanDE@users.noreply.github.com> Date: Sat, 21 Dec 2024 13:42:32 +0100 Subject: [PATCH 21/43] fix; made help command visible --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index 9880751..e89e13b 100644 --- a/index.js +++ b/index.js @@ -126,7 +126,7 @@ client.on("interactionCreate", async (interaction) => { await interaction.reply({ content: `Available commands:\n${commandList}`, - ephemeral: true, + ephemeral: false, }); } From 3ecb81d1d92afcf53d3623d308948df54a11466d Mon Sep 17 00:00:00 2001 From: SimplyJanDE <78982850+SimplyJanDE@users.noreply.github.com> Date: Mon, 23 Dec 2024 13:31:37 +0100 Subject: [PATCH 22/43] feat; new closeissue command --- index.js | 87 +++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 83 insertions(+), 4 deletions(-) diff --git a/index.js b/index.js index e89e13b..d3b2e67 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,6 @@ require("dotenv").config(); -const { Client, GatewayIntentBits, REST, Routes, ChannelType, MessageCollector } = require("discord.js"); -const axios = require("axios"); +const { Client, GatewayIntentBits, REST, Routes, ChannelType, MessageCollector } = require ("discord.js"); +const axios = require ("axios"); // GitHub API base URL and repo data const GITHUB_API_BASE = "https://api.github.com"; @@ -46,7 +46,7 @@ const client = new Client({ // Register slash commands const registerCommands = async () => { - const releaseChoices = await fetchReleases(); + await fetchReleases(); commands = [ { @@ -81,6 +81,32 @@ const registerCommands = async () => { name: "repo", description: "Get the link to the GitHub repository." }, + { + name: "closeissue", + description: "Close an issue on GitHub and lock the thread.", + options: [ + { + name: "state", + type: 3, // String + description: "The state to set for the GitHub issue (e.g., open, closed).", + required: true, + choices: [ + { name: "Open", value: "open" }, + { name: "Closed", value: "closed" }, + ], + }, + { + name: "state_reason", + type: 3, // String + description: "The reason for closing the GitHub issue.", + required: true, + choices: [ + { name: "Completed", value: "completed" }, + { name: "Not Planned", value: "not_planned" }, + ], + }, + ], + }, ]; const rest = new REST({ version: "10" }).setToken(process.env.DISCORD_TOKEN); @@ -285,8 +311,61 @@ ${screenshotsText} // Start asking questions askQuestion(); } + + if (commandName === "closeissue") { + const allowedRoles = ["Developer", "Administrator"]; + const state = options.getString("state"); + const stateReason = options.getString("state_reason"); + const thread = interaction.channel; + + // Check if the user has the required role + const memberRoles = interaction.member.roles.cache.map((role) => role.name); + if (!memberRoles.some((role) => allowedRoles.includes(role))) { + await interaction.reply({ content: "❌ You do not have permission to use this command.", ephemeral: true }); + return; + } + + // Check if the command is executed in a forum thread + if (thread.type !== ChannelType.PublicThread && thread.type !== ChannelType.PrivateThread) { + await interaction.reply({ content: "❌ This command can only be used in a forum thread.", ephemeral: true }); + return; + } + + try { + const messages = await thread.messages.fetch({ limit: 100 }); + const githubLinkMessage = messages.find( + (msg) => msg.content.includes("https://github.com") && msg.content.includes("/issues/") + ); + + if (!githubLinkMessage) { + await interaction.reply({ content: "❌ No GitHub link found in the thread.", ephemeral: true }); + return; + } + + const issueUrlMatch = githubLinkMessage.content.match(/\/issues\/(\d+)/); + if (!issueUrlMatch) { + await interaction.reply({ content: "❌ Invalid GitHub link in the thread.", ephemeral: true }); + return; + } + + const issueNumber = issueUrlMatch[1]; + + await axios.patch( + `${GITHUB_API_BASE}/repos/${REPO_OWNER}/${REPO_NAME}/issues/${issueNumber}`, + { state, state_reason: stateReason }, + { headers: { Authorization: `token ${GITHUB_TOKEN}` } } + ); + + await thread.setLocked(true, "Thread closed by developer."); + await thread.send(`βœ… This issue has been resolved and the GitHub issue is now "${state}" with reason "${stateReason}".`); + await interaction.reply({ content: "βœ… Issue closed successfully.", ephemeral: true }); + } catch (error) { + console.error("Error closing issue:", error); + await interaction.reply({ content: "❌ Failed to close the issue. Please try again.", ephemeral: true }); + } + } }); // Register commands and start the bot registerCommands(); -client.login(process.env.DISCORD_TOKEN); +client.login(process.env.DISCORD_TOKEN); \ No newline at end of file From 56b0d985e01425a0dbb37b2da829a105a5290dac Mon Sep 17 00:00:00 2001 From: SimplyJanDE <78982850+SimplyJanDE@users.noreply.github.com> Date: Tue, 24 Dec 2024 12:44:42 +0100 Subject: [PATCH 23/43] fix; thread renamed after creation --- index.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/index.js b/index.js index d3b2e67..2dbd799 100644 --- a/index.js +++ b/index.js @@ -297,6 +297,15 @@ ${screenshotsText} collectedData[question.key] = "Screenshots uploaded"; } else { collectedData[question.key] = msg.content; + + if (question.key ==="title") { + try { + await thread.setName(`Issue ${collectedData.title} reported by ${interaction.user.username}`); + } catch (error) { + console.error("Error setting thread name:", error); + await thread.send("❌ Failed to set the thread name. Please try again."); + } + } } askQuestion(questionIndex + 1); }); From 8673dcc960668963dc98cd10a2352c2e3e86732f Mon Sep 17 00:00:00 2001 From: SimplyJanDE <78982850+SimplyJanDE@users.noreply.github.com> Date: Tue, 24 Dec 2024 12:47:30 +0100 Subject: [PATCH 24/43] wip --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index 2dbd799..77cc260 100644 --- a/index.js +++ b/index.js @@ -210,7 +210,7 @@ client.on("interactionCreate", async (interaction) => { await interaction.reply({ content: `βœ… Forum thread created, please fill out the issue report: [${thread.name}](https://discord.com/channels/${interaction.guild.id}/${forumChannelId}/${thread.id})`, - ephemeral: true, + ephemeral: false, }); // Define the questions to ask the user From 51871b9d91060b4508b6cf13fbe03f77ca311a01 Mon Sep 17 00:00:00 2001 From: SimplyJanDE <78982850+SimplyJanDE@users.noreply.github.com> Date: Fri, 27 Dec 2024 18:17:56 +0100 Subject: [PATCH 25/43] feat: Add command to create a thread for feature requests - Added a new command "featurerequest" that creates a thread in the specified channel. - Users can describe their feature requests in the created thread. - Updated the "donate" command message to thank users for their support. - Fixed minor issues and improved error handling in the "closeissue" command. --- index.js | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/index.js b/index.js index 77cc260..1d44764 100644 --- a/index.js +++ b/index.js @@ -107,6 +107,14 @@ const registerCommands = async () => { }, ], }, + { + name: "featurerequest", + description: "Request a new feature for Streamyfin.", + }, + { + name:"donate", + description: "Shows how to support the Streamyfin project." + } ]; const rest = new REST({ version: "10" }).setToken(process.env.DISCORD_TOKEN); @@ -373,6 +381,30 @@ ${screenshotsText} await interaction.reply({ content: "❌ Failed to close the issue. Please try again.", ephemeral: true }); } } + + + + if (commandName === "donate") { + await interaction.reply({ + content: `🎁 Thank you for supporting our work by sharing your experiences. While we do have many contributors, the main work is made by <@${userId}>! Best way to support his effords is by spend him a coffee: https://buymeacoffee.com/fredrikbur3`, + }); + } + + if (commandName ==="featurerequest") { + const targetChannel = client.channels.cache.get('1273278866105831424'); + if (!targetChannel) { + await interaction.reply({ content:'❌ Target channel not found.', ephemeral: true}); + return; + } + + const thread = await targetChannel.threads.create({ + name: `Feature Request by ${interaction.user.username}`, + reason: 'User requested a feature', + }); + + await thread.send(' ${interaction.user.username} has a feature request. Please share your idea and any additional information here.'); + await interaction.reply({content: 'βœ… Your feature request thread has been created in the #features channel!', ephemeral: true }); + } }); // Register commands and start the bot From be31f0b2dc6f6c1a48ef916c88a2a6f3ab9fd0fd Mon Sep 17 00:00:00 2001 From: SimplyJanDE <78982850+SimplyJanDE@users.noreply.github.com> Date: Fri, 27 Dec 2024 18:31:04 +0100 Subject: [PATCH 26/43] fix: Define userId globally to avoid reference errors - Moved the definition of userId to the top of the file for global access. - Ensured userId is available for multiple commands, including "donate" and "testflight". - Improved error handling and messaging for the "featurerequest" command. --- index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index 1d44764..74e91af 100644 --- a/index.js +++ b/index.js @@ -8,6 +8,8 @@ const REPO_OWNER = process.env.REPO_OWNER; const REPO_NAME = process.env.REPO_NAME; const GITHUB_TOKEN = process.env.GITHUB_TOKEN; +const userId = '398161771476549654'; + let commands = []; // Global commands array // Function to fetch releases from GitHub @@ -187,7 +189,6 @@ client.on("interactionCreate", async (interaction) => { } if (commandName === "testflight") { - const userId = '398161771476549654'; await interaction.reply( `Currently, Streamyfin Testflight is full. However, you can send a private message to <@${userId}> with your email address and he will add you to the Testflight beta group manually.` ); From fd2ac357b2d5a8b1272ba34a7dad7e79f18965f7 Mon Sep 17 00:00:00 2001 From: SimplyJanDE <78982850+SimplyJanDE@users.noreply.github.com> Date: Sat, 28 Dec 2024 12:16:30 +0100 Subject: [PATCH 27/43] feat: Update featurerequest command to include description option - Modified the "featurerequest" command to accept a description option. - The command now creates a thread in the specified channel with the provided feature description. - Users can share more details about their feature request in the created thread. --- index.js | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/index.js b/index.js index 74e91af..df2a894 100644 --- a/index.js +++ b/index.js @@ -112,6 +112,14 @@ const registerCommands = async () => { { name: "featurerequest", description: "Request a new feature for Streamyfin.", + options: [ + { + name: "description", + type: 3, // String + description: "A short description of the feature request.", + required: true, + }, + ], }, { name:"donate", @@ -387,11 +395,12 @@ ${screenshotsText} if (commandName === "donate") { await interaction.reply({ - content: `🎁 Thank you for supporting our work by sharing your experiences. While we do have many contributors, the main work is made by <@${userId}>! Best way to support his effords is by spend him a coffee: https://buymeacoffee.com/fredrikbur3`, + content: `🎁 Thank you for supporting our work and sharing your experiences! While many contributors are involved, the majority of the work is done by <@${userId}>. The best way to show your support is by buying him a coffee: https://buymeacoffee.com/fredrikbur3`, }); - } + } if (commandName ==="featurerequest") { + const description = options.getString("description"); const targetChannel = client.channels.cache.get('1273278866105831424'); if (!targetChannel) { await interaction.reply({ content:'❌ Target channel not found.', ephemeral: true}); @@ -399,12 +408,12 @@ ${screenshotsText} } const thread = await targetChannel.threads.create({ - name: `Feature Request by ${interaction.user.username}`, + name: `Feature: ${description} requested by ${interaction.user.username},`, reason: 'User requested a feature', }); - await thread.send(' ${interaction.user.username} has a feature request. Please share your idea and any additional information here.'); - await interaction.reply({content: 'βœ… Your feature request thread has been created in the #features channel!', ephemeral: true }); + await thread.send({content: `πŸŽ‰ Thank you for your feature request! Feel free to discuss this feature here!`}); + await interaction.reply({content: `βœ… Your feature request has been submitted and a discussion thread has been created: [${thread.name}](https://discord.com/channels/${interaction.guild.id}/${targetChannel.id}/${thread.id})`, ephemeral: true}); } }); From 06c2ff89478867fa578c0db9e9c78620d66d666f Mon Sep 17 00:00:00 2001 From: retardgerman <78982850+retardgerman@users.noreply.github.com> Date: Sat, 28 Dec 2024 14:10:19 +0100 Subject: [PATCH 28/43] fix: remove trailing comma --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index df2a894..fc2bad7 100644 --- a/index.js +++ b/index.js @@ -408,7 +408,7 @@ ${screenshotsText} } const thread = await targetChannel.threads.create({ - name: `Feature: ${description} requested by ${interaction.user.username},`, + name: `Feature: ${description} requested by ${interaction.user.username}`, reason: 'User requested a feature', }); From cbb0d0c0b812ddc6fc75024cdd8b59c2f5cb1629 Mon Sep 17 00:00:00 2001 From: retardgerman <78982850+retardgerman@users.noreply.github.com> Date: Mon, 30 Dec 2024 20:46:47 +0100 Subject: [PATCH 29/43] feat: add proper README --- README.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1f2f29c..8114cf6 100644 --- a/README.md +++ b/README.md @@ -1 +1,15 @@ -# streamyfin-discord-bot +# Streamyfin Discord Bot + +A Discord bot to interact with the Streamyfin GitHub repository and streamline issue management, feature requests, and other project-related tasks. + +--- + +## Features + +- Fetch the latest releases from the GitHub repository. +- Provide links to the GitHub repository and roadmap. +- Create and close issues directly from Discord. +- Submit feature requests and start discussions in dedicated threads. +- Provide a list of all available commands. +- Share information about joining the Testflight beta. +- Allow users to donate to support the project. From 24ca6ae5e7956f9b0db5e4c733e4d14b89334220 Mon Sep 17 00:00:00 2001 From: retardgerman <78982850+retardgerman@users.noreply.github.com> Date: Mon, 30 Dec 2024 20:50:26 +0100 Subject: [PATCH 30/43] fix: README Header --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8114cf6..62355a7 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Streamyfin Discord Bot +# [Streamyfin](https://github.com/streamyfin/streamyfin)-Discord Bot A Discord bot to interact with the Streamyfin GitHub repository and streamline issue management, feature requests, and other project-related tasks. From c67efedc026fd5a1b907ef08c74d65a1047fe70f Mon Sep 17 00:00:00 2001 From: sldless <67599596+sldless@users.noreply.github.com> Date: Mon, 30 Dec 2024 20:40:29 -0500 Subject: [PATCH 31/43] Added stats commad --- index.js | 105 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 101 insertions(+), 4 deletions(-) diff --git a/index.js b/index.js index fc2bad7..ae6c8b7 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,5 @@ require("dotenv").config(); -const { Client, GatewayIntentBits, REST, Routes, ChannelType, MessageCollector } = require ("discord.js"); +const { Client, GatewayIntentBits, REST, Routes, ChannelType, MessageCollector, StringSelectMenuBuilder } = require ("discord.js"); const axios = require ("axios"); // GitHub API base URL and repo data @@ -41,6 +41,18 @@ const fetchReleases = async () => { } }; +const fetchStats = async () => { + const url = `https://api.github.com/repos/streamyfin/streamyfin/contributors?anon=1`; + + const response = await axios.get(url); + const contributors = await response.data; + + return contributors.map((contributor) => ({ + username: contributor.login || contributor.name, + contributions: contributor.contributions, + })); +}; + // Initialize Discord client const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent], @@ -48,7 +60,7 @@ const client = new Client({ // Register slash commands const registerCommands = async () => { - await fetchReleases(); + if (GITHUB_TOKEN) await fetchReleases(); commands = [ { @@ -124,6 +136,10 @@ const registerCommands = async () => { { name:"donate", description: "Shows how to support the Streamyfin project." + }, + { + name: "stats", + description: "Streamyfin's stats" } ]; @@ -415,8 +431,89 @@ ${screenshotsText} await thread.send({content: `πŸŽ‰ Thank you for your feature request! Feel free to discuss this feature here!`}); await interaction.reply({content: `βœ… Your feature request has been submitted and a discussion thread has been created: [${thread.name}](https://discord.com/channels/${interaction.guild.id}/${targetChannel.id}/${thread.id})`, ephemeral: true}); } -}); + if (commandName === "stats") { + + const leaderboard = await fetchStats(); + if (!leaderboard || leaderboard.length === 0) { + await interaction.reply({ content: "❌ No data available", ephemeral: true }); + return; + } + const mapped = leaderboard.map((x, index) => `${index + 1}. ${x.username} - ${x.contributions}`) .join("\n"); + + const repoResponse = await axios.get("https://api.github.com/repos/streamyfin/streamyfin"); + const starCount = repoResponse.data.stargazers_count; + let embed = { + color: 0x6A0DAD, + title: "πŸ“ˆ Contribution Overview", + description: mapped, + fields: [ + { + name: "⭐ Star Count", + value: `The repository has **${starCount}** stars.`, + inline: true, + } + ], + timestamp: new Date(), + footer: { + text: `Star Count: ${starCount}`, + }, + }; + let options = [ + { + label: "Information", + value: "Information", + description: "Get information about the repo!" + } + ] + let menu = new StringSelectMenuBuilder() + .setCustomId(`Menu_${interaction.user.id}`) + .setPlaceholder("Select a choice!") + .setMinValues(1) + .setMaxValues(1) + .addOptions(options); + let msg = await interaction.reply({ embeds: [embed], ephemeral: false, components: [{ type: 1, components: [menu] }] }); + let filter = (msg) => interaction.user.id === msg.user.id; + let collector = msg.createMessageComponentCollector({ filter, max: 1, errors: ["time"], time: 120000 }); + collector.on("collect", (interaction) => { + embed = { + title: "Streamyfin's info", + color: 0x6A0DAD, + description: repoResponse.data.description, + thumbnail: { + url: repoResponse.data.organization.avatar_url + }, + fields: [ + { + name: "Forks", + value: repoResponse.data.forks_count.toLocaleString(), + inline: true + }, + { + name: "Watchers", + value: repoResponse.data.watchers.toLocaleString(), + inline: true, + }, + { + name: "Stars", + value: starCount, + inline: true, + }, + { + name: "Language", + value: repoResponse.data.language, + inline: true, + }, + { + name: "License", + value: repoResponse.data.license.name, + inline: true, + } + ] + } + interaction.update({ embeds: [embed] }); + }) +}}) // Register commands and start the bot registerCommands(); -client.login(process.env.DISCORD_TOKEN); \ No newline at end of file +client.login(process.env.DISCORD_TOKEN); From 211aa5b71c69c83519ed58df74ccf298990f4d63 Mon Sep 17 00:00:00 2001 From: sldless <67599596+sldless@users.noreply.github.com> Date: Tue, 31 Dec 2024 04:04:35 -0500 Subject: [PATCH 32/43] Command Handler --- client.js | 52 +++++++++++ commands/bot/donate.js | 12 +++ commands/bot/help.js | 17 ++++ commands/bot/testflight.js | 12 +++ commands/github/closeissue.js | 81 +++++++++++++++++ commands/github/createissue.js | 146 ++++++++++++++++++++++++++++++ commands/github/featurerequest.js | 23 +++++ commands/github/issue.js | 36 ++++++++ commands/github/repo.js | 12 +++ commands/github/roadmap.js | 12 +++ commands/github/stats.js | 90 ++++++++++++++++++ newindex.js | 61 +++++++++++++ 12 files changed, 554 insertions(+) create mode 100644 client.js create mode 100644 commands/bot/donate.js create mode 100644 commands/bot/help.js create mode 100644 commands/bot/testflight.js create mode 100644 commands/github/closeissue.js create mode 100644 commands/github/createissue.js create mode 100644 commands/github/featurerequest.js create mode 100644 commands/github/issue.js create mode 100644 commands/github/repo.js create mode 100644 commands/github/roadmap.js create mode 100644 commands/github/stats.js create mode 100644 newindex.js diff --git a/client.js b/client.js new file mode 100644 index 0000000..e625e15 --- /dev/null +++ b/client.js @@ -0,0 +1,52 @@ +const { Client, Collection, EmbedBuilder } = require("discord.js"); +const axios = require ("axios"); + +module.exports = class Streamyfin extends Client { + constructor(...options) { + super(...options); + + this.commands = new Collection(); + this.userId = '398161771476549654'; + + } + + async fetchStats() { + const url = `https://api.github.com/repos/streamyfin/streamyfin/contributors?anon=1`; + + const response = await axios.get(url); + const contributors = await response.data; + + return contributors.map((contributor) => ({ + username: contributor.login || contributor.name, + contributions: contributor.contributions, + })); + }; + + async fetchReleases(){ + try { + const response = await axios.get( + `${GITHUB_API_BASE}/repos/${REPO_OWNER}/${REPO_NAME}/releases`, + { + headers: { + Authorization: `token ${GITHUB_TOKEN}`, + }, + } + ); + + const releases = response.data + .slice(0, 2) // Fetch the latest 2 releases + .map((release) => ({ name: release.name, value: release.name })); + + releases.push({ name: "Older", value: "Older" }); // Add "Older" as an option + + return releases; + } catch (error) { + console.error("Error fetching releases:", error); + return [ + { name: "0.22.0", value: "0.22.0" }, // Fallback data + { name: "0.21.0", value: "0.21.0" }, + { name: "Older", value: "Older" }, + ]; + } + }; +}; \ No newline at end of file diff --git a/commands/bot/donate.js b/commands/bot/donate.js new file mode 100644 index 0000000..bc20328 --- /dev/null +++ b/commands/bot/donate.js @@ -0,0 +1,12 @@ +const { SlashCommandBuilder } = require('discord.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('donate') + .setDescription('Shows how to support the Streamyfin project.'), + async run(interaction) { + await interaction.reply({ + content: `🎁 Thank you for supporting our work and sharing your experiences! While many contributors are involved, the majority of the work is done by <@${interaction.client.userId}>. The best way to show your support is by buying him a coffee: https://buymeacoffee.com/fredrikbur3`, + }); + }, +}; \ No newline at end of file diff --git a/commands/bot/help.js b/commands/bot/help.js new file mode 100644 index 0000000..2610fc8 --- /dev/null +++ b/commands/bot/help.js @@ -0,0 +1,17 @@ +const { SlashCommandBuilder } = require('discord.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('help') + .setDescription('Get a list of available commands.'), + async run(interaction) { + const commandList = commands + .map((cmd) => `**/${cmd.name}**: ${cmd.description}`) + .join("\n"); + + await interaction.reply({ + content: `Available commands:\n${commandList}`, + ephemeral: false, + }); + }, +}; \ No newline at end of file diff --git a/commands/bot/testflight.js b/commands/bot/testflight.js new file mode 100644 index 0000000..a856bb0 --- /dev/null +++ b/commands/bot/testflight.js @@ -0,0 +1,12 @@ +const { SlashCommandBuilder } = require('discord.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('testflight') + .setDescription('Explains how to join the Streamyfin Testflight.'), + async run(interaction) { + await interaction.reply( + `Currently, Streamyfin Testflight is full. However, you can send a private message to <@${interaction.client.userId}> with your email address and he will add you to the Testflight beta group manually.` + ); + }, +}; \ No newline at end of file diff --git a/commands/github/closeissue.js b/commands/github/closeissue.js new file mode 100644 index 0000000..687cb16 --- /dev/null +++ b/commands/github/closeissue.js @@ -0,0 +1,81 @@ +const { SlashCommandBuilder } = require('discord.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('closeissue') + .setDescription('Close an issue on GitHub and lock the thread.') + .addStringOption(option => + option.setName('state') + .setDescription('The state to set for the GitHub issue (e.g., open, closed).') + .setRequired(true) + .addChoices( + { name: 'Open', value: 'open' }, + { name: 'Closed', value: 'closed' } + ) + ) + .addStringOption(option => + option.setName('state_reason') + .setDescription('The reason for closing the GitHub issue.') + .setRequired(true) + .addChoices( + { name: 'Completed', value: 'completed' }, + { name: 'Not Planned', value: 'not_planned' } + ) + ), + async run(interaction) { + const GITHUB_API_BASE = "https://api.github.com"; + const REPO_OWNER = process.env.REPO_OWNER; + const REPO_NAME = process.env.REPO_NAME; + const GITHUB_TOKEN = process.env.GITHUB_TOKEN; + const allowedRoles = ["Developer", "Administrator"]; + const state = options.getString("state"); + const stateReason = options.getString("state_reason"); + const thread = interaction.channel; + + // Check if the user has the required role + const memberRoles = interaction.member.roles.cache.map((role) => role.name); + if (!memberRoles.some((role) => allowedRoles.includes(role))) { + await interaction.reply({ content: "❌ You do not have permission to use this command.", ephemeral: true }); + return; + } + + // Check if the command is executed in a forum thread + if (thread.type !== ChannelType.PublicThread && thread.type !== ChannelType.PrivateThread) { + await interaction.reply({ content: "❌ This command can only be used in a forum thread.", ephemeral: true }); + return; + } + + try { + const messages = await thread.messages.fetch({ limit: 100 }); + const githubLinkMessage = messages.find( + (msg) => msg.content.includes("https://github.com") && msg.content.includes("/issues/") + ); + + if (!githubLinkMessage) { + await interaction.reply({ content: "❌ No GitHub link found in the thread.", ephemeral: true }); + return; + } + + const issueUrlMatch = githubLinkMessage.content.match(/\/issues\/(\d+)/); + if (!issueUrlMatch) { + await interaction.reply({ content: "❌ Invalid GitHub link in the thread.", ephemeral: true }); + return; + } + + const issueNumber = issueUrlMatch[1]; + + await axios.patch( + `${GITHUB_API_BASE}/repos/${REPO_OWNER}/${REPO_NAME}/issues/${issueNumber}`, + { state, state_reason: stateReason }, + { headers: { Authorization: `token ${GITHUB_TOKEN}` } } + ); + + await thread.setLocked(true, "Thread closed by developer."); + await thread.send(`βœ… This issue has been resolved and the GitHub issue is now "${state}" with reason "${stateReason}".`); + await interaction.reply({ content: "βœ… Issue closed successfully.", ephemeral: true }); + } catch (error) { + console.error("Error closing issue:", error); + await interaction.reply({ content: "❌ Failed to close the issue. Please try again.", ephemeral: true }); + } + }, +}; \ No newline at end of file diff --git a/commands/github/createissue.js b/commands/github/createissue.js new file mode 100644 index 0000000..d1eb0ba --- /dev/null +++ b/commands/github/createissue.js @@ -0,0 +1,146 @@ +const { SlashCommandBuilder } = require('discord.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('createissue') + .setDescription('Create a new issue on GitHub.'), + async run(interaction) { + const REPO_OWNER = process.env.REPO_OWNER; + const GITHUB_API_BASE = "https://api.github.com"; + const REPO_NAME = process.env.REPO_NAME; + const GITHUB_TOKEN = process.env.GITHUB_TOKEN; + // Start by creating a private thread in forum + const forumChannelId = process.env.FORUM_CHANNEL_ID; + const forumChannel = interaction.guild.channels.cache.get(forumChannelId); + + if (!forumChannel || forumChannel.type !== ChannelType.GuildForum) { + await interaction.reply({ + content: "❌ Forum channel not found or is not a forum channel.", + ephemeral: true, + }); + return; + } + + const thread = await forumChannel.threads.create({ + name: `Issue Report by ${interaction.user.username}`, + message: { + content: `Hello ${interaction.user.username}, let's collect the details for your issue report!`, + }, + autoArchiveDuration: 60, // Auto-archive after 1 hour + type: ChannelType.PrivateThread, + reason: "Collecting issue details", + }); + + await interaction.reply({ + content: `βœ… Forum thread created, please fill out the issue report: [${thread.name}](https://discord.com/channels/${interaction.guild.id}/${forumChannelId}/${thread.id})`, + ephemeral: false, + }); + + // Define the questions to ask the user + const questions = [ + { key: "title", question: "Describe your issue in a few words." }, + { key: "description", question: "What happened? What did you expect to happen?" }, + { key: "steps", question: "How can this issue be reproduced? (step-by-step)" }, + { key: "device", question: "What device and operating system are you using?" }, + { key: "version", question: "What is the affected Streamyfin version?" }, + { key: "screenshots", question: "Please provide any screenshots that might help us reproduce the issue (optional), or type 'none'.", allowUploads: true }, + ]; + + const collectedData = {}; // Store user responses here + const uploadedFiles = []; // Store uploaded files here + // Helper function to ask questions + const askQuestion = async (questionIndex = 0) => { + if (questionIndex >= questions.length) { + // All questions have been asked + const screenshotsText = uploadedFiles.length + ? uploadedFiles.map((file) => file.url).join("\n") + : "No screenshots provided"; + + const body = ` + ### What happened? + ${collectedData.description} + + ### Reproduction steps + ${collectedData.steps} + + ### Device and operating system + ${collectedData.device} + + ### Version + ${collectedData.version} + + ### Screenshots + ${screenshotsText} + `; + + try { + const issueResponse = await axios.post( + `${GITHUB_API_BASE}/repos/${REPO_OWNER}/${REPO_NAME}/issues`, + { + title: `[Bug]: ${collectedData.title} reported via Discord by [${interaction.user.username}]`, + body: body, + labels: ["❌ bug"], + assignees: ["fredrikburmester"], + }, + { + headers: { + Authorization: `token ${GITHUB_TOKEN}`, + }, + } + ); + + await thread.send(`βœ… Issue created successfully: ${issueResponse.data.html_url}`); + await thread.setLocked(true, "Issue details collected and sent to GitHub."); + } catch (error) { + console.error("Error creating issue:", error); + await thread.send("❌ Failed to create the issue. Please try again."); + } + return; + } + + // Ask the next question + const question = questions[questionIndex]; + await thread.send(question.question); + + // Set up a message collector to get the user's response + const collector = new MessageCollector(thread, { + filter: (msg) => msg.author.id === interaction.user.id, + max: 1, // Collect only one message + time: 300000, // Timeout after 5 minutes + }); + + collector.on("collect", async (msg) => { + if (question.allowUploads && msg.attachments.size > 0) { + msg.attachments.forEach((attachment) => { + uploadedFiles.push({ + name: attachment.filename, + url: attachment.url, + }); + }); + collectedData[question.key] = "Screenshots uploaded"; + } else { + collectedData[question.key] = msg.content; + + if (question.key === "title") { + try { + await thread.setName(`Issue ${collectedData.title} reported by ${interaction.user.username}`); + } catch (error) { + console.error("Error setting thread name:", error); + await thread.send("❌ Failed to set the thread name. Please try again."); + } + } + } + askQuestion(questionIndex + 1); + }); + + collector.on("end", (collected, reason) => { + if (reason === "time") { + thread.send("❌ You did not respond in time. Please run the command again if you'd like to create an issue."); + } + }); + }; + + // Start asking questions + askQuestion(); + }, +}; \ No newline at end of file diff --git a/commands/github/featurerequest.js b/commands/github/featurerequest.js new file mode 100644 index 0000000..50c374d --- /dev/null +++ b/commands/github/featurerequest.js @@ -0,0 +1,23 @@ +const { SlashCommandBuilder } = require('discord.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('featurerequest') + .setDescription('Request a new feature for Streamyfin.'), + async run(interaction) { + const description = options.getString("description"); + const targetChannel = client.channels.cache.get('1273278866105831424'); + if (!targetChannel) { + await interaction.reply({ content: '❌ Target channel not found.', ephemeral: true }); + return; + } + + const thread = await targetChannel.threads.create({ + name: `Feature: ${description} requested by ${interaction.user.username}`, + reason: 'User requested a feature', + }); + + await thread.send({ content: `πŸŽ‰ Thank you for your feature request! Feel free to discuss this feature here!` }); + await interaction.reply({ content: `βœ… Your feature request has been submitted and a discussion thread has been created: [${thread.name}](https://discord.com/channels/${interaction.guild.id}/${targetChannel.id}/${thread.id})`, ephemeral: true }); + }, +}; \ No newline at end of file diff --git a/commands/github/issue.js b/commands/github/issue.js new file mode 100644 index 0000000..438a9a1 --- /dev/null +++ b/commands/github/issue.js @@ -0,0 +1,36 @@ +const { SlashCommandBuilder } = require('discord.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('issue') + .setDescription('Get details about a specific issue from GitHub.') + .addIntegerOption(option => + option.setName('number') + .setDescription('The issue number') + .setRequired(true) + ), + async run(interaction) { + const REPO_OWNER = process.env.REPO_OWNER; + const REPO_NAME = process.env.REPO_NAME; + const GITHUB_TOKEN = process.env.GITHUB_TOKEN; + const issueNumber = interaction.options.getInteger("number"); + + try { + const response = await axios.get( + `https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/issues/${issueNumber}`, + { + headers: { + Authorization: `token ${GITHUB_TOKEN}`, + }, + } + ); + + const issue = response.data; + await interaction.reply( + `πŸ”— **Issue #${issue.number}: ${issue.title}**\n${issue.html_url}` + ); + } catch (error) { + await interaction.reply("❌ Issue not found or an error occurred."); + } + }, +}; \ No newline at end of file diff --git a/commands/github/repo.js b/commands/github/repo.js new file mode 100644 index 0000000..a6d4d7b --- /dev/null +++ b/commands/github/repo.js @@ -0,0 +1,12 @@ +const { SlashCommandBuilder } = require('discord.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('repo') + .setDescription('Get the link to the GitHub repository.'), + async run(interaction) { + await interaction.reply( + "πŸ“‘ Here is our GitHub repository: " + ); + }, +}; \ No newline at end of file diff --git a/commands/github/roadmap.js b/commands/github/roadmap.js new file mode 100644 index 0000000..87dca67 --- /dev/null +++ b/commands/github/roadmap.js @@ -0,0 +1,12 @@ +const { SlashCommandBuilder } = require('discord.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('roadmap') + .setDescription('Get the link to the GitHub roadmap.'), + async run(interaction) { + await interaction.reply( + "πŸ“Œ Here is our Roadmap: " + ); + }, +}; \ No newline at end of file diff --git a/commands/github/stats.js b/commands/github/stats.js new file mode 100644 index 0000000..3d2f099 --- /dev/null +++ b/commands/github/stats.js @@ -0,0 +1,90 @@ +const { SlashCommandBuilder } = require('discord.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('stats') + .setDescription('Shows how to support the Streamyfin project.'), + async run(interaction) { + + const leaderboard = await fetchStats(); + if (!leaderboard || leaderboard.length === 0) { + await interaction.reply({ content: "❌ No data available", ephemeral: true }); + return; + } + const mapped = leaderboard.map((x, index) => `${index + 1}. ${x.username} - ${x.contributions}`).join("\n"); + + const repoResponse = await axios.get("https://api.github.com/repos/streamyfin/streamyfin"); + const starCount = repoResponse.data.stargazers_count; + + let embed = { + color: 0x6A0DAD, + title: "πŸ“ˆ Contribution Overview", + description: mapped, + fields: [ + { + name: "⭐ Star Count", + value: `The repository has **${starCount}** stars.`, + inline: true, + } + ], + timestamp: new Date(), + footer: { + text: `Star Count: ${starCount}`, + }, + }; + let options = [ + { + label: "Information", + value: "Information", + description: "Get information about the repo!" + } + ] + let menu = new StringSelectMenuBuilder() + .setCustomId(`Menu_${interaction.user.id}`) + .setPlaceholder("Select a choice!") + .setMinValues(1) + .setMaxValues(1) + .addOptions(options); + let msg = await interaction.reply({ embeds: [embed], ephemeral: false, components: [{ type: 1, components: [menu] }] }); + let filter = (msg) => interaction.user.id === msg.user.id; + let collector = msg.createMessageComponentCollector({ filter, max: 1, errors: ["time"], time: 120000 }); + collector.on("collect", (interaction) => { + embed = { + title: "Streamyfin's info", + color: 0x6A0DAD, + description: repoResponse.data.description, + thumbnail: { + url: repoResponse.data.organization.avatar_url + }, + fields: [ + { + name: "Forks", + value: repoResponse.data.forks_count.toLocaleString(), + inline: true + }, + { + name: "Watchers", + value: repoResponse.data.watchers.toLocaleString(), + inline: true, + }, + { + name: "Stars", + value: starCount, + inline: true, + }, + { + name: "Language", + value: repoResponse.data.language, + inline: true, + }, + { + name: "License", + value: repoResponse.data.license.name, + inline: true, + } + ] + } + interaction.update({ embeds: [embed] }); + }) + }, +}; \ No newline at end of file diff --git a/newindex.js b/newindex.js new file mode 100644 index 0000000..1cb8f89 --- /dev/null +++ b/newindex.js @@ -0,0 +1,61 @@ +require("dotenv").config(); +const Streamyfin = require('./client'); +const { Client, GatewayIntentBits, REST, Routes, ChannelType, MessageCollector, StringSelectMenuBuilder } = require ("discord.js"); +const axios = require ("axios"); +const fs = require("fs"); + +// GitHub API base URL and repo data +const GITHUB_TOKEN = process.env.GITHUB_TOKEN; + +const tempCommands = [] + +// Initialize Discord client +const client = new Streamyfin({ + intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent], +}); + +fs.readdirSync("./commands/").forEach(dir => { + const files = fs.readdirSync(`./commands/${dir}/`).filter(file => file.endsWith(".js")); + for (let file of files) { + let props = require(`./commands/${dir}/${file}`); + client.commands.set(props.data.name, props); + tempCommands.push(props.data) + console.log(`[COMMAND] => Loaded ${file} `); + } +}); + +client.on("interactionCreate", async (interaction) => { + if (!interaction.isCommand()) return; + + const command = client.commands.get(interaction.commandName); + if (!command) return; + + try { + await command.run(interaction); + } catch (error) { + console.error(error); + await interaction.reply({ + content: 'There was an error while executing this command!', + ephemeral: true, + }); + } +}) +const registerCommands = async () => { + if (GITHUB_TOKEN) await client.fetchReleases(); + + const rest = new REST({ version: "10" }).setToken(process.env.DISCORD_TOKEN); + + try { + console.log("Registering slash commands..."); + await rest.put(Routes.applicationCommands(process.env.CLIENT_ID), { + body: tempCommands, + }); + console.log("Slash commands registered successfully!"); + } catch (error) { + console.error("Error registering slash commands:", error); + } +}; + +registerCommands(); +client.login(process.env.DISCORD_TOKEN); + From 25e8d098fd7ae75963e8c93e2923e90e5e61109a Mon Sep 17 00:00:00 2001 From: sldless <67599596+sldless@users.noreply.github.com> Date: Tue, 31 Dec 2024 04:06:31 -0500 Subject: [PATCH 33/43] File rename (index.js) --- index.js | 526 ++++------------------------------------------------ newindex.js | 61 ------ oldIndex.js | 519 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 553 insertions(+), 553 deletions(-) delete mode 100644 newindex.js create mode 100644 oldIndex.js diff --git a/index.js b/index.js index ae6c8b7..1cb8f89 100644 --- a/index.js +++ b/index.js @@ -1,154 +1,54 @@ require("dotenv").config(); +const Streamyfin = require('./client'); const { Client, GatewayIntentBits, REST, Routes, ChannelType, MessageCollector, StringSelectMenuBuilder } = require ("discord.js"); const axios = require ("axios"); +const fs = require("fs"); // GitHub API base URL and repo data -const GITHUB_API_BASE = "https://api.github.com"; -const REPO_OWNER = process.env.REPO_OWNER; -const REPO_NAME = process.env.REPO_NAME; const GITHUB_TOKEN = process.env.GITHUB_TOKEN; -const userId = '398161771476549654'; - -let commands = []; // Global commands array - -// Function to fetch releases from GitHub -const fetchReleases = async () => { - try { - const response = await axios.get( - `${GITHUB_API_BASE}/repos/${REPO_OWNER}/${REPO_NAME}/releases`, - { - headers: { - Authorization: `token ${GITHUB_TOKEN}`, - }, - } - ); - - const releases = response.data - .slice(0, 2) // Fetch the latest 2 releases - .map((release) => ({ name: release.name, value: release.name })); - - releases.push({ name: "Older", value: "Older" }); // Add "Older" as an option - - return releases; - } catch (error) { - console.error("Error fetching releases:", error); - return [ - { name: "0.22.0", value: "0.22.0" }, // Fallback data - { name: "0.21.0", value: "0.21.0" }, - { name: "Older", value: "Older" }, - ]; - } -}; - -const fetchStats = async () => { - const url = `https://api.github.com/repos/streamyfin/streamyfin/contributors?anon=1`; - - const response = await axios.get(url); - const contributors = await response.data; - - return contributors.map((contributor) => ({ - username: contributor.login || contributor.name, - contributions: contributor.contributions, - })); -}; +const tempCommands = [] // Initialize Discord client -const client = new Client({ - intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent], +const client = new Streamyfin({ + intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent], +}); + +fs.readdirSync("./commands/").forEach(dir => { + const files = fs.readdirSync(`./commands/${dir}/`).filter(file => file.endsWith(".js")); + for (let file of files) { + let props = require(`./commands/${dir}/${file}`); + client.commands.set(props.data.name, props); + tempCommands.push(props.data) + console.log(`[COMMAND] => Loaded ${file} `); + } }); -// Register slash commands +client.on("interactionCreate", async (interaction) => { + if (!interaction.isCommand()) return; + + const command = client.commands.get(interaction.commandName); + if (!command) return; + + try { + await command.run(interaction); + } catch (error) { + console.error(error); + await interaction.reply({ + content: 'There was an error while executing this command!', + ephemeral: true, + }); + } +}) const registerCommands = async () => { - if (GITHUB_TOKEN) await fetchReleases(); - - commands = [ - { - name: "roadmap", - description: "Get the link to the GitHub roadmap.", - }, - { - name: "issue", - description: "Get details about a specific issue from GitHub.", - options: [ - { - name: "number", - type: 4, // Integer - description: "The issue number", - required: true, - }, - ], - }, - { - name: "createissue", - description: "Create a new issue on GitHub.", - }, - { - name: "help", - description: "Get a list of available commands.", - }, - { - name: "testflight", - description: "Explains how to join the Streamyfin Testflight.", - }, - { - name: "repo", - description: "Get the link to the GitHub repository." - }, - { - name: "closeissue", - description: "Close an issue on GitHub and lock the thread.", - options: [ - { - name: "state", - type: 3, // String - description: "The state to set for the GitHub issue (e.g., open, closed).", - required: true, - choices: [ - { name: "Open", value: "open" }, - { name: "Closed", value: "closed" }, - ], - }, - { - name: "state_reason", - type: 3, // String - description: "The reason for closing the GitHub issue.", - required: true, - choices: [ - { name: "Completed", value: "completed" }, - { name: "Not Planned", value: "not_planned" }, - ], - }, - ], - }, - { - name: "featurerequest", - description: "Request a new feature for Streamyfin.", - options: [ - { - name: "description", - type: 3, // String - description: "A short description of the feature request.", - required: true, - }, - ], - }, - { - name:"donate", - description: "Shows how to support the Streamyfin project." - }, - { - name: "stats", - description: "Streamyfin's stats" - } - ]; + if (GITHUB_TOKEN) await client.fetchReleases(); const rest = new REST({ version: "10" }).setToken(process.env.DISCORD_TOKEN); try { console.log("Registering slash commands..."); await rest.put(Routes.applicationCommands(process.env.CLIENT_ID), { - body: commands, + body: tempCommands, }); console.log("Slash commands registered successfully!"); } catch (error) { @@ -156,364 +56,6 @@ const registerCommands = async () => { } }; -// Event when the bot is ready -client.once("ready", () => { - console.log(`Logged in as ${client.user.tag}!`); -}); - -// Handle slash command interactions -client.on("interactionCreate", async (interaction) => { - if (!interaction.isCommand()) return; - - const { commandName, options } = interaction; - - if (commandName === "repo") { - await interaction.reply( - "πŸ“‘ Here is our GitHub repository: https://github.com/fredrikburmester/streamyfin" - ); - } - - if (commandName === "roadmap") { - await interaction.reply( - "πŸ“Œ Here is our Roadmap: https://github.com/users/fredrikburmester/projects/5/views/8" - ); - } - - if (commandName === "help") { - const commandList = commands - .map((cmd) => `**/${cmd.name}**: ${cmd.description}`) - .join("\n"); - - await interaction.reply({ - content: `Available commands:\n${commandList}`, - ephemeral: false, - }); - } - - if (commandName === "issue") { - const issueNumber = options.getInteger("number"); - - try { - const response = await axios.get( - `${GITHUB_API_BASE}/repos/${REPO_OWNER}/${REPO_NAME}/issues/${issueNumber}`, - { - headers: { - Authorization: `token ${GITHUB_TOKEN}`, - }, - } - ); - - const issue = response.data; - await interaction.reply( - `πŸ”— **Issue #${issue.number}: ${issue.title}**\n${issue.html_url}` - ); - } catch (error) { - await interaction.reply("❌ Issue not found or an error occurred."); - } - } - - if (commandName === "testflight") { - await interaction.reply( - `Currently, Streamyfin Testflight is full. However, you can send a private message to <@${userId}> with your email address and he will add you to the Testflight beta group manually.` - ); - } - - if (commandName === "createissue") { - // Start by creating a private thread in forum - const forumChannelId= process.env.FORUM_CHANNEL_ID; - const forumChannel = interaction.guild.channels.cache.get(forumChannelId); - - if (!forumChannel || forumChannel.type !== ChannelType.GuildForum) { - await interaction.reply({ - content: "❌ Forum channel not found or is not a forum channel.", - ephemeral: true, - }); - return; - } - - const thread = await forumChannel.threads.create({ - name: `Issue Report by ${interaction.user.username}`, - message: { - content: `Hello ${interaction.user.username}, let's collect the details for your issue report!`, - }, - autoArchiveDuration: 60, // Auto-archive after 1 hour - type: ChannelType.PrivateThread, - reason: "Collecting issue details", - }); - - await interaction.reply({ - content: `βœ… Forum thread created, please fill out the issue report: [${thread.name}](https://discord.com/channels/${interaction.guild.id}/${forumChannelId}/${thread.id})`, - ephemeral: false, - }); - - // Define the questions to ask the user - const questions = [ - { key: "title", question: "Describe your issue in a few words." }, - { key: "description", question: "What happened? What did you expect to happen?" }, - { key: "steps", question: "How can this issue be reproduced? (step-by-step)" }, - { key: "device", question: "What device and operating system are you using?" }, - { key: "version", question: "What is the affected Streamyfin version?" }, - { key: "screenshots", question: "Please provide any screenshots that might help us reproduce the issue (optional), or type 'none'.", allowUploads: true }, - ]; - - const collectedData = {}; // Store user responses here - const uploadedFiles = []; // Store uploaded files here - // Helper function to ask questions - const askQuestion = async (questionIndex = 0) => { - if (questionIndex >= questions.length) { - // All questions have been asked - const screenshotsText = uploadedFiles.length - ? uploadedFiles.map((file) => file.url).join("\n") - : "No screenshots provided"; - - const body = ` -### What happened? -${collectedData.description} - -### Reproduction steps -${collectedData.steps} - -### Device and operating system -${collectedData.device} - -### Version -${collectedData.version} - -### Screenshots -${screenshotsText} -`; - - try { - const issueResponse = await axios.post( - `${GITHUB_API_BASE}/repos/${REPO_OWNER}/${REPO_NAME}/issues`, - { - title: `[Bug]: ${collectedData.title} reported via Discord by [${interaction.user.username}]`, - body: body, - labels: ["❌ bug"], - assignees: ["fredrikburmester"], - }, - { - headers: { - Authorization: `token ${GITHUB_TOKEN}`, - }, - } - ); - - await thread.send(`βœ… Issue created successfully: ${issueResponse.data.html_url}`); - await thread.setLocked(true, "Issue details collected and sent to GitHub."); - } catch (error) { - console.error("Error creating issue:", error); - await thread.send("❌ Failed to create the issue. Please try again."); - } - return; - } - - // Ask the next question - const question = questions[questionIndex]; - await thread.send(question.question); - - // Set up a message collector to get the user's response - const collector = new MessageCollector(thread, { - filter: (msg) => msg.author.id === interaction.user.id, - max: 1, // Collect only one message - time: 300000, // Timeout after 5 minutes - }); - - collector.on("collect", async (msg) => { - if (question.allowUploads && msg.attachments.size > 0) { - msg.attachments.forEach((attachment) => { - uploadedFiles.push({ - name: attachment.filename, - url: attachment.url, - }); - }); - collectedData[question.key] = "Screenshots uploaded"; - } else { - collectedData[question.key] = msg.content; - - if (question.key ==="title") { - try { - await thread.setName(`Issue ${collectedData.title} reported by ${interaction.user.username}`); - } catch (error) { - console.error("Error setting thread name:", error); - await thread.send("❌ Failed to set the thread name. Please try again."); - } - } - } - askQuestion(questionIndex + 1); - }); - - collector.on("end", (collected, reason) => { - if (reason === "time") { - thread.send("❌ You did not respond in time. Please run the command again if you'd like to create an issue."); - } - }); - }; - - // Start asking questions - askQuestion(); - } - - if (commandName === "closeissue") { - const allowedRoles = ["Developer", "Administrator"]; - const state = options.getString("state"); - const stateReason = options.getString("state_reason"); - const thread = interaction.channel; - - // Check if the user has the required role - const memberRoles = interaction.member.roles.cache.map((role) => role.name); - if (!memberRoles.some((role) => allowedRoles.includes(role))) { - await interaction.reply({ content: "❌ You do not have permission to use this command.", ephemeral: true }); - return; - } - - // Check if the command is executed in a forum thread - if (thread.type !== ChannelType.PublicThread && thread.type !== ChannelType.PrivateThread) { - await interaction.reply({ content: "❌ This command can only be used in a forum thread.", ephemeral: true }); - return; - } - - try { - const messages = await thread.messages.fetch({ limit: 100 }); - const githubLinkMessage = messages.find( - (msg) => msg.content.includes("https://github.com") && msg.content.includes("/issues/") - ); - - if (!githubLinkMessage) { - await interaction.reply({ content: "❌ No GitHub link found in the thread.", ephemeral: true }); - return; - } - - const issueUrlMatch = githubLinkMessage.content.match(/\/issues\/(\d+)/); - if (!issueUrlMatch) { - await interaction.reply({ content: "❌ Invalid GitHub link in the thread.", ephemeral: true }); - return; - } - - const issueNumber = issueUrlMatch[1]; - - await axios.patch( - `${GITHUB_API_BASE}/repos/${REPO_OWNER}/${REPO_NAME}/issues/${issueNumber}`, - { state, state_reason: stateReason }, - { headers: { Authorization: `token ${GITHUB_TOKEN}` } } - ); - - await thread.setLocked(true, "Thread closed by developer."); - await thread.send(`βœ… This issue has been resolved and the GitHub issue is now "${state}" with reason "${stateReason}".`); - await interaction.reply({ content: "βœ… Issue closed successfully.", ephemeral: true }); - } catch (error) { - console.error("Error closing issue:", error); - await interaction.reply({ content: "❌ Failed to close the issue. Please try again.", ephemeral: true }); - } - } - - - - if (commandName === "donate") { - await interaction.reply({ - content: `🎁 Thank you for supporting our work and sharing your experiences! While many contributors are involved, the majority of the work is done by <@${userId}>. The best way to show your support is by buying him a coffee: https://buymeacoffee.com/fredrikbur3`, - }); - } - - if (commandName ==="featurerequest") { - const description = options.getString("description"); - const targetChannel = client.channels.cache.get('1273278866105831424'); - if (!targetChannel) { - await interaction.reply({ content:'❌ Target channel not found.', ephemeral: true}); - return; - } - - const thread = await targetChannel.threads.create({ - name: `Feature: ${description} requested by ${interaction.user.username}`, - reason: 'User requested a feature', - }); - - await thread.send({content: `πŸŽ‰ Thank you for your feature request! Feel free to discuss this feature here!`}); - await interaction.reply({content: `βœ… Your feature request has been submitted and a discussion thread has been created: [${thread.name}](https://discord.com/channels/${interaction.guild.id}/${targetChannel.id}/${thread.id})`, ephemeral: true}); - } - if (commandName === "stats") { - - const leaderboard = await fetchStats(); - if (!leaderboard || leaderboard.length === 0) { - await interaction.reply({ content: "❌ No data available", ephemeral: true }); - return; - } - const mapped = leaderboard.map((x, index) => `${index + 1}. ${x.username} - ${x.contributions}`) .join("\n"); - - const repoResponse = await axios.get("https://api.github.com/repos/streamyfin/streamyfin"); - const starCount = repoResponse.data.stargazers_count; - - let embed = { - color: 0x6A0DAD, - title: "πŸ“ˆ Contribution Overview", - description: mapped, - fields: [ - { - name: "⭐ Star Count", - value: `The repository has **${starCount}** stars.`, - inline: true, - } - ], - timestamp: new Date(), - footer: { - text: `Star Count: ${starCount}`, - }, - }; - let options = [ - { - label: "Information", - value: "Information", - description: "Get information about the repo!" - } - ] - let menu = new StringSelectMenuBuilder() - .setCustomId(`Menu_${interaction.user.id}`) - .setPlaceholder("Select a choice!") - .setMinValues(1) - .setMaxValues(1) - .addOptions(options); - let msg = await interaction.reply({ embeds: [embed], ephemeral: false, components: [{ type: 1, components: [menu] }] }); - let filter = (msg) => interaction.user.id === msg.user.id; - let collector = msg.createMessageComponentCollector({ filter, max: 1, errors: ["time"], time: 120000 }); - collector.on("collect", (interaction) => { - embed = { - title: "Streamyfin's info", - color: 0x6A0DAD, - description: repoResponse.data.description, - thumbnail: { - url: repoResponse.data.organization.avatar_url - }, - fields: [ - { - name: "Forks", - value: repoResponse.data.forks_count.toLocaleString(), - inline: true - }, - { - name: "Watchers", - value: repoResponse.data.watchers.toLocaleString(), - inline: true, - }, - { - name: "Stars", - value: starCount, - inline: true, - }, - { - name: "Language", - value: repoResponse.data.language, - inline: true, - }, - { - name: "License", - value: repoResponse.data.license.name, - inline: true, - } - ] - } - interaction.update({ embeds: [embed] }); - }) -}}) -// Register commands and start the bot registerCommands(); client.login(process.env.DISCORD_TOKEN); + diff --git a/newindex.js b/newindex.js deleted file mode 100644 index 1cb8f89..0000000 --- a/newindex.js +++ /dev/null @@ -1,61 +0,0 @@ -require("dotenv").config(); -const Streamyfin = require('./client'); -const { Client, GatewayIntentBits, REST, Routes, ChannelType, MessageCollector, StringSelectMenuBuilder } = require ("discord.js"); -const axios = require ("axios"); -const fs = require("fs"); - -// GitHub API base URL and repo data -const GITHUB_TOKEN = process.env.GITHUB_TOKEN; - -const tempCommands = [] - -// Initialize Discord client -const client = new Streamyfin({ - intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent], -}); - -fs.readdirSync("./commands/").forEach(dir => { - const files = fs.readdirSync(`./commands/${dir}/`).filter(file => file.endsWith(".js")); - for (let file of files) { - let props = require(`./commands/${dir}/${file}`); - client.commands.set(props.data.name, props); - tempCommands.push(props.data) - console.log(`[COMMAND] => Loaded ${file} `); - } -}); - -client.on("interactionCreate", async (interaction) => { - if (!interaction.isCommand()) return; - - const command = client.commands.get(interaction.commandName); - if (!command) return; - - try { - await command.run(interaction); - } catch (error) { - console.error(error); - await interaction.reply({ - content: 'There was an error while executing this command!', - ephemeral: true, - }); - } -}) -const registerCommands = async () => { - if (GITHUB_TOKEN) await client.fetchReleases(); - - const rest = new REST({ version: "10" }).setToken(process.env.DISCORD_TOKEN); - - try { - console.log("Registering slash commands..."); - await rest.put(Routes.applicationCommands(process.env.CLIENT_ID), { - body: tempCommands, - }); - console.log("Slash commands registered successfully!"); - } catch (error) { - console.error("Error registering slash commands:", error); - } -}; - -registerCommands(); -client.login(process.env.DISCORD_TOKEN); - diff --git a/oldIndex.js b/oldIndex.js new file mode 100644 index 0000000..ae6c8b7 --- /dev/null +++ b/oldIndex.js @@ -0,0 +1,519 @@ +require("dotenv").config(); +const { Client, GatewayIntentBits, REST, Routes, ChannelType, MessageCollector, StringSelectMenuBuilder } = require ("discord.js"); +const axios = require ("axios"); + +// GitHub API base URL and repo data +const GITHUB_API_BASE = "https://api.github.com"; +const REPO_OWNER = process.env.REPO_OWNER; +const REPO_NAME = process.env.REPO_NAME; +const GITHUB_TOKEN = process.env.GITHUB_TOKEN; + +const userId = '398161771476549654'; + +let commands = []; // Global commands array + +// Function to fetch releases from GitHub +const fetchReleases = async () => { + try { + const response = await axios.get( + `${GITHUB_API_BASE}/repos/${REPO_OWNER}/${REPO_NAME}/releases`, + { + headers: { + Authorization: `token ${GITHUB_TOKEN}`, + }, + } + ); + + const releases = response.data + .slice(0, 2) // Fetch the latest 2 releases + .map((release) => ({ name: release.name, value: release.name })); + + releases.push({ name: "Older", value: "Older" }); // Add "Older" as an option + + return releases; + } catch (error) { + console.error("Error fetching releases:", error); + return [ + { name: "0.22.0", value: "0.22.0" }, // Fallback data + { name: "0.21.0", value: "0.21.0" }, + { name: "Older", value: "Older" }, + ]; + } +}; + +const fetchStats = async () => { + const url = `https://api.github.com/repos/streamyfin/streamyfin/contributors?anon=1`; + + const response = await axios.get(url); + const contributors = await response.data; + + return contributors.map((contributor) => ({ + username: contributor.login || contributor.name, + contributions: contributor.contributions, + })); +}; + +// Initialize Discord client +const client = new Client({ + intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent], +}); + +// Register slash commands +const registerCommands = async () => { + if (GITHUB_TOKEN) await fetchReleases(); + + commands = [ + { + name: "roadmap", + description: "Get the link to the GitHub roadmap.", + }, + { + name: "issue", + description: "Get details about a specific issue from GitHub.", + options: [ + { + name: "number", + type: 4, // Integer + description: "The issue number", + required: true, + }, + ], + }, + { + name: "createissue", + description: "Create a new issue on GitHub.", + }, + { + name: "help", + description: "Get a list of available commands.", + }, + { + name: "testflight", + description: "Explains how to join the Streamyfin Testflight.", + }, + { + name: "repo", + description: "Get the link to the GitHub repository." + }, + { + name: "closeissue", + description: "Close an issue on GitHub and lock the thread.", + options: [ + { + name: "state", + type: 3, // String + description: "The state to set for the GitHub issue (e.g., open, closed).", + required: true, + choices: [ + { name: "Open", value: "open" }, + { name: "Closed", value: "closed" }, + ], + }, + { + name: "state_reason", + type: 3, // String + description: "The reason for closing the GitHub issue.", + required: true, + choices: [ + { name: "Completed", value: "completed" }, + { name: "Not Planned", value: "not_planned" }, + ], + }, + ], + }, + { + name: "featurerequest", + description: "Request a new feature for Streamyfin.", + options: [ + { + name: "description", + type: 3, // String + description: "A short description of the feature request.", + required: true, + }, + ], + }, + { + name:"donate", + description: "Shows how to support the Streamyfin project." + }, + { + name: "stats", + description: "Streamyfin's stats" + } + ]; + + const rest = new REST({ version: "10" }).setToken(process.env.DISCORD_TOKEN); + + try { + console.log("Registering slash commands..."); + await rest.put(Routes.applicationCommands(process.env.CLIENT_ID), { + body: commands, + }); + console.log("Slash commands registered successfully!"); + } catch (error) { + console.error("Error registering slash commands:", error); + } +}; + +// Event when the bot is ready +client.once("ready", () => { + console.log(`Logged in as ${client.user.tag}!`); +}); + +// Handle slash command interactions +client.on("interactionCreate", async (interaction) => { + if (!interaction.isCommand()) return; + + const { commandName, options } = interaction; + + if (commandName === "repo") { + await interaction.reply( + "πŸ“‘ Here is our GitHub repository: https://github.com/fredrikburmester/streamyfin" + ); + } + + if (commandName === "roadmap") { + await interaction.reply( + "πŸ“Œ Here is our Roadmap: https://github.com/users/fredrikburmester/projects/5/views/8" + ); + } + + if (commandName === "help") { + const commandList = commands + .map((cmd) => `**/${cmd.name}**: ${cmd.description}`) + .join("\n"); + + await interaction.reply({ + content: `Available commands:\n${commandList}`, + ephemeral: false, + }); + } + + if (commandName === "issue") { + const issueNumber = options.getInteger("number"); + + try { + const response = await axios.get( + `${GITHUB_API_BASE}/repos/${REPO_OWNER}/${REPO_NAME}/issues/${issueNumber}`, + { + headers: { + Authorization: `token ${GITHUB_TOKEN}`, + }, + } + ); + + const issue = response.data; + await interaction.reply( + `πŸ”— **Issue #${issue.number}: ${issue.title}**\n${issue.html_url}` + ); + } catch (error) { + await interaction.reply("❌ Issue not found or an error occurred."); + } + } + + if (commandName === "testflight") { + await interaction.reply( + `Currently, Streamyfin Testflight is full. However, you can send a private message to <@${userId}> with your email address and he will add you to the Testflight beta group manually.` + ); + } + + if (commandName === "createissue") { + // Start by creating a private thread in forum + const forumChannelId= process.env.FORUM_CHANNEL_ID; + const forumChannel = interaction.guild.channels.cache.get(forumChannelId); + + if (!forumChannel || forumChannel.type !== ChannelType.GuildForum) { + await interaction.reply({ + content: "❌ Forum channel not found or is not a forum channel.", + ephemeral: true, + }); + return; + } + + const thread = await forumChannel.threads.create({ + name: `Issue Report by ${interaction.user.username}`, + message: { + content: `Hello ${interaction.user.username}, let's collect the details for your issue report!`, + }, + autoArchiveDuration: 60, // Auto-archive after 1 hour + type: ChannelType.PrivateThread, + reason: "Collecting issue details", + }); + + await interaction.reply({ + content: `βœ… Forum thread created, please fill out the issue report: [${thread.name}](https://discord.com/channels/${interaction.guild.id}/${forumChannelId}/${thread.id})`, + ephemeral: false, + }); + + // Define the questions to ask the user + const questions = [ + { key: "title", question: "Describe your issue in a few words." }, + { key: "description", question: "What happened? What did you expect to happen?" }, + { key: "steps", question: "How can this issue be reproduced? (step-by-step)" }, + { key: "device", question: "What device and operating system are you using?" }, + { key: "version", question: "What is the affected Streamyfin version?" }, + { key: "screenshots", question: "Please provide any screenshots that might help us reproduce the issue (optional), or type 'none'.", allowUploads: true }, + ]; + + const collectedData = {}; // Store user responses here + const uploadedFiles = []; // Store uploaded files here + // Helper function to ask questions + const askQuestion = async (questionIndex = 0) => { + if (questionIndex >= questions.length) { + // All questions have been asked + const screenshotsText = uploadedFiles.length + ? uploadedFiles.map((file) => file.url).join("\n") + : "No screenshots provided"; + + const body = ` +### What happened? +${collectedData.description} + +### Reproduction steps +${collectedData.steps} + +### Device and operating system +${collectedData.device} + +### Version +${collectedData.version} + +### Screenshots +${screenshotsText} +`; + + try { + const issueResponse = await axios.post( + `${GITHUB_API_BASE}/repos/${REPO_OWNER}/${REPO_NAME}/issues`, + { + title: `[Bug]: ${collectedData.title} reported via Discord by [${interaction.user.username}]`, + body: body, + labels: ["❌ bug"], + assignees: ["fredrikburmester"], + }, + { + headers: { + Authorization: `token ${GITHUB_TOKEN}`, + }, + } + ); + + await thread.send(`βœ… Issue created successfully: ${issueResponse.data.html_url}`); + await thread.setLocked(true, "Issue details collected and sent to GitHub."); + } catch (error) { + console.error("Error creating issue:", error); + await thread.send("❌ Failed to create the issue. Please try again."); + } + return; + } + + // Ask the next question + const question = questions[questionIndex]; + await thread.send(question.question); + + // Set up a message collector to get the user's response + const collector = new MessageCollector(thread, { + filter: (msg) => msg.author.id === interaction.user.id, + max: 1, // Collect only one message + time: 300000, // Timeout after 5 minutes + }); + + collector.on("collect", async (msg) => { + if (question.allowUploads && msg.attachments.size > 0) { + msg.attachments.forEach((attachment) => { + uploadedFiles.push({ + name: attachment.filename, + url: attachment.url, + }); + }); + collectedData[question.key] = "Screenshots uploaded"; + } else { + collectedData[question.key] = msg.content; + + if (question.key ==="title") { + try { + await thread.setName(`Issue ${collectedData.title} reported by ${interaction.user.username}`); + } catch (error) { + console.error("Error setting thread name:", error); + await thread.send("❌ Failed to set the thread name. Please try again."); + } + } + } + askQuestion(questionIndex + 1); + }); + + collector.on("end", (collected, reason) => { + if (reason === "time") { + thread.send("❌ You did not respond in time. Please run the command again if you'd like to create an issue."); + } + }); + }; + + // Start asking questions + askQuestion(); + } + + if (commandName === "closeissue") { + const allowedRoles = ["Developer", "Administrator"]; + const state = options.getString("state"); + const stateReason = options.getString("state_reason"); + const thread = interaction.channel; + + // Check if the user has the required role + const memberRoles = interaction.member.roles.cache.map((role) => role.name); + if (!memberRoles.some((role) => allowedRoles.includes(role))) { + await interaction.reply({ content: "❌ You do not have permission to use this command.", ephemeral: true }); + return; + } + + // Check if the command is executed in a forum thread + if (thread.type !== ChannelType.PublicThread && thread.type !== ChannelType.PrivateThread) { + await interaction.reply({ content: "❌ This command can only be used in a forum thread.", ephemeral: true }); + return; + } + + try { + const messages = await thread.messages.fetch({ limit: 100 }); + const githubLinkMessage = messages.find( + (msg) => msg.content.includes("https://github.com") && msg.content.includes("/issues/") + ); + + if (!githubLinkMessage) { + await interaction.reply({ content: "❌ No GitHub link found in the thread.", ephemeral: true }); + return; + } + + const issueUrlMatch = githubLinkMessage.content.match(/\/issues\/(\d+)/); + if (!issueUrlMatch) { + await interaction.reply({ content: "❌ Invalid GitHub link in the thread.", ephemeral: true }); + return; + } + + const issueNumber = issueUrlMatch[1]; + + await axios.patch( + `${GITHUB_API_BASE}/repos/${REPO_OWNER}/${REPO_NAME}/issues/${issueNumber}`, + { state, state_reason: stateReason }, + { headers: { Authorization: `token ${GITHUB_TOKEN}` } } + ); + + await thread.setLocked(true, "Thread closed by developer."); + await thread.send(`βœ… This issue has been resolved and the GitHub issue is now "${state}" with reason "${stateReason}".`); + await interaction.reply({ content: "βœ… Issue closed successfully.", ephemeral: true }); + } catch (error) { + console.error("Error closing issue:", error); + await interaction.reply({ content: "❌ Failed to close the issue. Please try again.", ephemeral: true }); + } + } + + + + if (commandName === "donate") { + await interaction.reply({ + content: `🎁 Thank you for supporting our work and sharing your experiences! While many contributors are involved, the majority of the work is done by <@${userId}>. The best way to show your support is by buying him a coffee: https://buymeacoffee.com/fredrikbur3`, + }); + } + + if (commandName ==="featurerequest") { + const description = options.getString("description"); + const targetChannel = client.channels.cache.get('1273278866105831424'); + if (!targetChannel) { + await interaction.reply({ content:'❌ Target channel not found.', ephemeral: true}); + return; + } + + const thread = await targetChannel.threads.create({ + name: `Feature: ${description} requested by ${interaction.user.username}`, + reason: 'User requested a feature', + }); + + await thread.send({content: `πŸŽ‰ Thank you for your feature request! Feel free to discuss this feature here!`}); + await interaction.reply({content: `βœ… Your feature request has been submitted and a discussion thread has been created: [${thread.name}](https://discord.com/channels/${interaction.guild.id}/${targetChannel.id}/${thread.id})`, ephemeral: true}); + } + if (commandName === "stats") { + + const leaderboard = await fetchStats(); + if (!leaderboard || leaderboard.length === 0) { + await interaction.reply({ content: "❌ No data available", ephemeral: true }); + return; + } + const mapped = leaderboard.map((x, index) => `${index + 1}. ${x.username} - ${x.contributions}`) .join("\n"); + + const repoResponse = await axios.get("https://api.github.com/repos/streamyfin/streamyfin"); + const starCount = repoResponse.data.stargazers_count; + + let embed = { + color: 0x6A0DAD, + title: "πŸ“ˆ Contribution Overview", + description: mapped, + fields: [ + { + name: "⭐ Star Count", + value: `The repository has **${starCount}** stars.`, + inline: true, + } + ], + timestamp: new Date(), + footer: { + text: `Star Count: ${starCount}`, + }, + }; + let options = [ + { + label: "Information", + value: "Information", + description: "Get information about the repo!" + } + ] + let menu = new StringSelectMenuBuilder() + .setCustomId(`Menu_${interaction.user.id}`) + .setPlaceholder("Select a choice!") + .setMinValues(1) + .setMaxValues(1) + .addOptions(options); + let msg = await interaction.reply({ embeds: [embed], ephemeral: false, components: [{ type: 1, components: [menu] }] }); + let filter = (msg) => interaction.user.id === msg.user.id; + let collector = msg.createMessageComponentCollector({ filter, max: 1, errors: ["time"], time: 120000 }); + collector.on("collect", (interaction) => { + embed = { + title: "Streamyfin's info", + color: 0x6A0DAD, + description: repoResponse.data.description, + thumbnail: { + url: repoResponse.data.organization.avatar_url + }, + fields: [ + { + name: "Forks", + value: repoResponse.data.forks_count.toLocaleString(), + inline: true + }, + { + name: "Watchers", + value: repoResponse.data.watchers.toLocaleString(), + inline: true, + }, + { + name: "Stars", + value: starCount, + inline: true, + }, + { + name: "Language", + value: repoResponse.data.language, + inline: true, + }, + { + name: "License", + value: repoResponse.data.license.name, + inline: true, + } + ] + } + interaction.update({ embeds: [embed] }); + }) +}}) +// Register commands and start the bot +registerCommands(); +client.login(process.env.DISCORD_TOKEN); From a8dfb8431cc1e51bd24b2cb2d66aa1812ff53a8c Mon Sep 17 00:00:00 2001 From: SimplyJanDE <78982850+SimplyJanDE@users.noreply.github.com> Date: Tue, 31 Dec 2024 13:10:22 +0100 Subject: [PATCH 34/43] fix: repo_owner and new API key --- index.js | 437 ++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 401 insertions(+), 36 deletions(-) diff --git a/index.js b/index.js index 1cb8f89..3be71ce 100644 --- a/index.js +++ b/index.js @@ -1,54 +1,142 @@ require("dotenv").config(); -const Streamyfin = require('./client'); -const { Client, GatewayIntentBits, REST, Routes, ChannelType, MessageCollector, StringSelectMenuBuilder } = require ("discord.js"); +const { Client, GatewayIntentBits, REST, Routes, ChannelType, MessageCollector } = require ("discord.js"); const axios = require ("axios"); -const fs = require("fs"); // GitHub API base URL and repo data +const GITHUB_API_BASE = "https://api.github.com"; +const REPO_OWNER = process.env.REPO_OWNER; +const REPO_NAME = process.env.REPO_NAME; const GITHUB_TOKEN = process.env.GITHUB_TOKEN; -const tempCommands = [] +const userId = '398161771476549654'; + +let commands = []; // Global commands array + +// Function to fetch releases from GitHub +const fetchReleases = async () => { + try { + const response = await axios.get( + `${GITHUB_API_BASE}/repos/${REPO_OWNER}/${REPO_NAME}/releases`, + { + headers: { + Authorization: `token ${GITHUB_TOKEN}`, + }, + } + ); + + const releases = response.data + .slice(0, 2) // Fetch the latest 2 releases + .map((release) => ({ name: release.name, value: release.name })); + + releases.push({ name: "Older", value: "Older" }); // Add "Older" as an option + + return releases; + } catch (error) { + console.error("Error fetching releases:", error); + return [ + { name: "0.22.0", value: "0.22.0" }, // Fallback data + { name: "0.21.0", value: "0.21.0" }, + { name: "Older", value: "Older" }, + ]; + } +}; // Initialize Discord client -const client = new Streamyfin({ - intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent], -}); - -fs.readdirSync("./commands/").forEach(dir => { - const files = fs.readdirSync(`./commands/${dir}/`).filter(file => file.endsWith(".js")); - for (let file of files) { - let props = require(`./commands/${dir}/${file}`); - client.commands.set(props.data.name, props); - tempCommands.push(props.data) - console.log(`[COMMAND] => Loaded ${file} `); - } +const client = new Client({ + intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent], }); -client.on("interactionCreate", async (interaction) => { - if (!interaction.isCommand()) return; - - const command = client.commands.get(interaction.commandName); - if (!command) return; - - try { - await command.run(interaction); - } catch (error) { - console.error(error); - await interaction.reply({ - content: 'There was an error while executing this command!', - ephemeral: true, - }); - } -}) +// Register slash commands const registerCommands = async () => { - if (GITHUB_TOKEN) await client.fetchReleases(); + await fetchReleases(); + + commands = [ + { + name: "roadmap", + description: "Get the link to the GitHub roadmap.", + }, + { + name: "issue", + description: "Get details about a specific issue from GitHub.", + options: [ + { + name: "number", + type: 4, // Integer + description: "The issue number", + required: true, + }, + ], + }, + { + name: "createissue", + description: "Create a new issue on GitHub.", + }, + { + name: "help", + description: "Get a list of available commands.", + }, + { + name: "testflight", + description: "Explains how to join the Streamyfin Testflight.", + }, + { + name: "repo", + description: "Get the link to the GitHub repository." + }, + { + name: "closeissue", + description: "Close an issue on GitHub and lock the thread.", + options: [ + { + name: "state", + type: 3, // String + description: "The state to set for the GitHub issue (e.g., open, closed).", + required: true, + choices: [ + { name: "Open", value: "open" }, + { name: "Closed", value: "closed" }, + ], + }, + { + name: "state_reason", + type: 3, // String + description: "The reason for closing the GitHub issue.", + required: true, + choices: [ + { name: "Completed", value: "completed" }, + { name: "Not Planned", value: "not_planned" }, + ], + }, + ], + }, + { + name: "featurerequest", + description: "Request a new feature for Streamyfin.", + options: [ + { + name: "description", + type: 3, // String + description: "A short description of the feature request.", + required: true, + }, + ], + }, + { + name:"donate", + description: "Shows how to support the Streamyfin project." + }, + { + name:"stats", + description: "Shows how to support the Streamyfin project." + }, + ]; const rest = new REST({ version: "10" }).setToken(process.env.DISCORD_TOKEN); try { console.log("Registering slash commands..."); await rest.put(Routes.applicationCommands(process.env.CLIENT_ID), { - body: tempCommands, + body: commands, }); console.log("Slash commands registered successfully!"); } catch (error) { @@ -56,6 +144,283 @@ const registerCommands = async () => { } }; -registerCommands(); -client.login(process.env.DISCORD_TOKEN); +// Event when the bot is ready +client.once("ready", () => { + console.log(`Logged in as ${client.user.tag}!`); +}); + +// Handle slash command interactions +client.on("interactionCreate", async (interaction) => { + if (!interaction.isCommand()) return; + + const { commandName, options } = interaction; + + if (commandName === "repo") { + await interaction.reply( + "πŸ“‘ Here is our GitHub repository: https://github.com/fredrikburmester/streamyfin" + ); + } + + if (commandName === "roadmap") { + await interaction.reply( + "πŸ“Œ Here is our Roadmap: https://github.com/users/fredrikburmester/projects/5/views/8" + ); + } + + if (commandName === "help") { + const commandList = commands + .map((cmd) => `**/${cmd.name}**: ${cmd.description}`) + .join("\n"); + + await interaction.reply({ + content: `Available commands:\n${commandList}`, + ephemeral: false, + }); + } + + if (commandName === "issue") { + const issueNumber = options.getInteger("number"); + + try { + const response = await axios.get( + `${GITHUB_API_BASE}/repos/${REPO_OWNER}/${REPO_NAME}/issues/${issueNumber}`, + { + headers: { + Authorization: `token ${GITHUB_TOKEN}`, + }, + } + ); + + const issue = response.data; + await interaction.reply( + `πŸ”— **Issue #${issue.number}: ${issue.title}**\n${issue.html_url}` + ); + } catch (error) { + await interaction.reply("❌ Issue not found or an error occurred."); + } + } + + if (commandName === "testflight") { + await interaction.reply( + `Currently, Streamyfin Testflight is full. However, you can send a private message to <@${userId}> with your email address and he will add you to the Testflight beta group manually.` + ); + } + + if (commandName === "createissue") { + // Start by creating a private thread in forum + const forumChannelId= process.env.FORUM_CHANNEL_ID; + const forumChannel = interaction.guild.channels.cache.get(forumChannelId); + + if (!forumChannel || forumChannel.type !== ChannelType.GuildForum) { + await interaction.reply({ + content: "❌ Forum channel not found or is not a forum channel.", + ephemeral: true, + }); + return; + } + + const thread = await forumChannel.threads.create({ + name: `Issue Report by ${interaction.user.username}`, + message: { + content: `Hello ${interaction.user.username}, let's collect the details for your issue report!`, + }, + autoArchiveDuration: 60, // Auto-archive after 1 hour + type: ChannelType.PrivateThread, + reason: "Collecting issue details", + }); + + await interaction.reply({ + content: `βœ… Forum thread created, please fill out the issue report: [${thread.name}](https://discord.com/channels/${interaction.guild.id}/${forumChannelId}/${thread.id})`, + ephemeral: false, + }); + + // Define the questions to ask the user + const questions = [ + { key: "title", question: "Describe your issue in a few words." }, + { key: "description", question: "What happened? What did you expect to happen?" }, + { key: "steps", question: "How can this issue be reproduced? (step-by-step)" }, + { key: "device", question: "What device and operating system are you using?" }, + { key: "version", question: "What is the affected Streamyfin version?" }, + { key: "screenshots", question: "Please provide any screenshots that might help us reproduce the issue (optional), or type 'none'.", allowUploads: true }, + ]; + const collectedData = {}; // Store user responses here + const uploadedFiles = []; // Store uploaded files here + // Helper function to ask questions + const askQuestion = async (questionIndex = 0) => { + if (questionIndex >= questions.length) { + // All questions have been asked + const screenshotsText = uploadedFiles.length + ? uploadedFiles.map((file) => file.url).join("\n") + : "No screenshots provided"; + + const body = ` +### What happened? +${collectedData.description} + +### Reproduction steps +${collectedData.steps} + +### Device and operating system +${collectedData.device} + +### Version +${collectedData.version} + +### Screenshots +${screenshotsText} +`; + + try { + const issueResponse = await axios.post( + `${GITHUB_API_BASE}/repos/${REPO_OWNER}/${REPO_NAME}/issues`, + { + title: `[Bug]: ${collectedData.title} reported via Discord by [${interaction.user.username}]`, + body: body, + labels: ["❌ bug"], + assignees: ["fredrikburmester"], + }, + { + headers: { + Authorization: `token ${GITHUB_TOKEN}`, + }, + } + ); + + await thread.send(`βœ… Issue created successfully: ${issueResponse.data.html_url}`); + await thread.setLocked(true, "Issue details collected and sent to GitHub."); + } catch (error) { + console.error("Error creating issue:", error); + await thread.send("❌ Failed to create the issue. Please try again."); + } + return; + } + + // Ask the next question + const question = questions[questionIndex]; + await thread.send(question.question); + + // Set up a message collector to get the user's response + const collector = new MessageCollector(thread, { + filter: (msg) => msg.author.id === interaction.user.id, + max: 1, // Collect only one message + time: 300000, // Timeout after 5 minutes + }); + + collector.on("collect", async (msg) => { + if (question.allowUploads && msg.attachments.size > 0) { + msg.attachments.forEach((attachment) => { + uploadedFiles.push({ + name: attachment.filename, + url: attachment.url, + }); + }); + collectedData[question.key] = "Screenshots uploaded"; + } else { + collectedData[question.key] = msg.content; + + if (question.key ==="title") { + try { + await thread.setName(`Issue ${collectedData.title} reported by ${interaction.user.username}`); + } catch (error) { + console.error("Error setting thread name:", error); + await thread.send("❌ Failed to set the thread name. Please try again."); + } + } + } + askQuestion(questionIndex + 1); + }); + + collector.on("end", (collected, reason) => { + if (reason === "time") { + thread.send("❌ You did not respond in time. Please run the command again if you'd like to create an issue."); + } + }); + }; + + // Start asking questions + askQuestion(); + } + + if (commandName === "closeissue") { + const allowedRoles = ["Developer", "Administrator"]; + const state = options.getString("state"); + const stateReason = options.getString("state_reason"); + const thread = interaction.channel; + + // Check if the user has the required role + const memberRoles = interaction.member.roles.cache.map((role) => role.name); + if (!memberRoles.some((role) => allowedRoles.includes(role))) { + await interaction.reply({ content: "❌ You do not have permission to use this command.", ephemeral: true }); + return; + } + + // Check if the command is executed in a forum thread + if (thread.type !== ChannelType.PublicThread && thread.type !== ChannelType.PrivateThread) { + await interaction.reply({ content: "❌ This command can only be used in a forum thread.", ephemeral: true }); + return; + } + + try { + const messages = await thread.messages.fetch({ limit: 100 }); + const githubLinkMessage = messages.find( + (msg) => msg.content.includes("https://github.com") && msg.content.includes("/issues/") + ); + + if (!githubLinkMessage) { + await interaction.reply({ content: "❌ No GitHub link found in the thread.", ephemeral: true }); + return; + } + + const issueUrlMatch = githubLinkMessage.content.match(/\/issues\/(\d+)/); + if (!issueUrlMatch) { + await interaction.reply({ content: "❌ Invalid GitHub link in the thread.", ephemeral: true }); + return; + } + + const issueNumber = issueUrlMatch[1]; + + await axios.patch( + `${GITHUB_API_BASE}/repos/${REPO_OWNER}/${REPO_NAME}/issues/${issueNumber}`, + { state, state_reason: stateReason }, + { headers: { Authorization: `token ${GITHUB_TOKEN}` } } + ); + + await thread.setLocked(true, "Thread closed by developer."); + await thread.send(`βœ… This issue has been resolved and the GitHub issue is now "${state}" with reason "${stateReason}".`); + await interaction.reply({ content: "βœ… Issue closed successfully.", ephemeral: true }); + } catch (error) { + console.error("Error closing issue:", error); + await interaction.reply({ content: "❌ Failed to close the issue. Please try again.", ephemeral: true }); + } + } + + + + if (commandName === "donate") { + await interaction.reply({ + content: `🎁 Thank you for supporting our work and sharing your experiences! While many contributors are involved, the majority of the work is done by <@${userId}>. The best way to show your support is by buying him a coffee: https://buymeacoffee.com/fredrikbur3`, + }); + } + + if (commandName ==="featurerequest") { + const description = options.getString("description"); + const targetChannel = client.channels.cache.get('1273278866105831424'); + if (!targetChannel) { + await interaction.reply({ content:'❌ Target channel not found.', ephemeral: true}); + return; + } + + const thread = await targetChannel.threads.create({ + name: `Feature: ${description} requested by ${interaction.user.username}`, + reason: 'User requested a feature', + }); + + await thread.send({content: `πŸŽ‰ Thank you for your feature request! Feel free to discuss this feature here!`}); + await interaction.reply({content: `βœ… Your feature request has been submitted and a discussion thread has been created: [${thread.name}](https://discord.com/channels/${interaction.guild.id}/${targetChannel.id}/${thread.id})`, ephemeral: true}); + } +}); + +// Register commands and start the bot +registerCommands(); +client.login(process.env.DISCORD_TOKEN); \ No newline at end of file From 9b66c0faf30794af3e6f5f7defb4e7d1c0670880 Mon Sep 17 00:00:00 2001 From: sldless <67599596+sldless@users.noreply.github.com> Date: Tue, 31 Dec 2024 12:03:36 -0500 Subject: [PATCH 35/43] Defined Most Things | Switched the order of viewing in stats.js --- commands/bot/help.js | 4 +- commands/github/closeissue.js | 7 +-- commands/github/createissue.js | 3 +- commands/github/featurerequest.js | 2 +- commands/github/issue.js | 1 + commands/github/stats.js | 90 +++++++++++++++---------------- index.js | 3 +- 7 files changed, 56 insertions(+), 54 deletions(-) diff --git a/commands/bot/help.js b/commands/bot/help.js index 2610fc8..f1ed998 100644 --- a/commands/bot/help.js +++ b/commands/bot/help.js @@ -5,8 +5,8 @@ module.exports = { .setName('help') .setDescription('Get a list of available commands.'), async run(interaction) { - const commandList = commands - .map((cmd) => `**/${cmd.name}**: ${cmd.description}`) + const commandList = interaction.client.commands + .map((cmd) => `**/${cmd.data.name}**: ${cmd.data.description}`) .join("\n"); await interaction.reply({ diff --git a/commands/github/closeissue.js b/commands/github/closeissue.js index 687cb16..79d99ab 100644 --- a/commands/github/closeissue.js +++ b/commands/github/closeissue.js @@ -1,4 +1,5 @@ -const { SlashCommandBuilder } = require('discord.js'); +const { SlashCommandBuilder, ChannelType } = require('discord.js'); +const axios = require ("axios"); module.exports = { data: new SlashCommandBuilder() @@ -28,8 +29,8 @@ module.exports = { const REPO_NAME = process.env.REPO_NAME; const GITHUB_TOKEN = process.env.GITHUB_TOKEN; const allowedRoles = ["Developer", "Administrator"]; - const state = options.getString("state"); - const stateReason = options.getString("state_reason"); + const state = interaction.options.getString("state"); + const stateReason = interaction.options.getString("state_reason"); const thread = interaction.channel; // Check if the user has the required role diff --git a/commands/github/createissue.js b/commands/github/createissue.js index d1eb0ba..d210400 100644 --- a/commands/github/createissue.js +++ b/commands/github/createissue.js @@ -1,4 +1,5 @@ -const { SlashCommandBuilder } = require('discord.js'); +const { SlashCommandBuilder, ChannelType, MessageCollector } = require('discord.js'); +const axios = require ("axios"); module.exports = { data: new SlashCommandBuilder() diff --git a/commands/github/featurerequest.js b/commands/github/featurerequest.js index 50c374d..57ccd73 100644 --- a/commands/github/featurerequest.js +++ b/commands/github/featurerequest.js @@ -6,7 +6,7 @@ module.exports = { .setDescription('Request a new feature for Streamyfin.'), async run(interaction) { const description = options.getString("description"); - const targetChannel = client.channels.cache.get('1273278866105831424'); + const targetChannel = interaction.client.channels.cache.get('1273278866105831424'); if (!targetChannel) { await interaction.reply({ content: '❌ Target channel not found.', ephemeral: true }); return; diff --git a/commands/github/issue.js b/commands/github/issue.js index 438a9a1..c683600 100644 --- a/commands/github/issue.js +++ b/commands/github/issue.js @@ -1,4 +1,5 @@ const { SlashCommandBuilder } = require('discord.js'); +const axios = require ("axios"); module.exports = { data: new SlashCommandBuilder() diff --git a/commands/github/stats.js b/commands/github/stats.js index 3d2f099..c65b5e6 100644 --- a/commands/github/stats.js +++ b/commands/github/stats.js @@ -1,4 +1,5 @@ -const { SlashCommandBuilder } = require('discord.js'); +const { SlashCommandBuilder, StringSelectMenuBuilder } = require('discord.js'); +const axios = require ("axios"); module.exports = { data: new SlashCommandBuilder() @@ -6,7 +7,7 @@ module.exports = { .setDescription('Shows how to support the Streamyfin project.'), async run(interaction) { - const leaderboard = await fetchStats(); + const leaderboard = await interaction.client.fetchStats(); if (!leaderboard || leaderboard.length === 0) { await interaction.reply({ content: "❌ No data available", ephemeral: true }); return; @@ -15,27 +16,45 @@ module.exports = { const repoResponse = await axios.get("https://api.github.com/repos/streamyfin/streamyfin"); const starCount = repoResponse.data.stargazers_count; - - let embed = { + let embed = { + title: "Streamyfin's info", color: 0x6A0DAD, - title: "πŸ“ˆ Contribution Overview", - description: mapped, + description: repoResponse.data.description, + thumbnail: { + url: repoResponse.data.organization.avatar_url + }, fields: [ { - name: "⭐ Star Count", - value: `The repository has **${starCount}** stars.`, + name: "Forks", + value: repoResponse.data.forks_count.toLocaleString(), + inline: true + }, + { + name: "Watchers", + value: repoResponse.data.watchers.toLocaleString(), + inline: true, + }, + { + name: "Stars", + value: starCount, + inline: true, + }, + { + name: "Language", + value: repoResponse.data.language, + inline: true, + }, + { + name: "License", + value: repoResponse.data.license.name, inline: true, } - ], - timestamp: new Date(), - footer: { - text: `Star Count: ${starCount}`, - }, - }; + ] + } let options = [ { - label: "Information", - value: "Information", + label: "πŸ“ˆ Contribution Overview", + value: "Contribution", description: "Get information about the repo!" } ] @@ -50,40 +69,21 @@ module.exports = { let collector = msg.createMessageComponentCollector({ filter, max: 1, errors: ["time"], time: 120000 }); collector.on("collect", (interaction) => { embed = { - title: "Streamyfin's info", color: 0x6A0DAD, - description: repoResponse.data.description, - thumbnail: { - url: repoResponse.data.organization.avatar_url - }, + title: "πŸ“ˆ Contribution Overview", + description: mapped, fields: [ { - name: "Forks", - value: repoResponse.data.forks_count.toLocaleString(), - inline: true - }, - { - name: "Watchers", - value: repoResponse.data.watchers.toLocaleString(), - inline: true, - }, - { - name: "Stars", - value: starCount, - inline: true, - }, - { - name: "Language", - value: repoResponse.data.language, - inline: true, - }, - { - name: "License", - value: repoResponse.data.license.name, + name: "⭐ Star Count", + value: `The repository has **${starCount}** stars.`, inline: true, } - ] - } + ], + timestamp: new Date(), + footer: { + text: `Star Count: ${starCount}`, + }, + }; interaction.update({ embeds: [embed] }); }) }, diff --git a/index.js b/index.js index 1cb8f89..b2c9778 100644 --- a/index.js +++ b/index.js @@ -1,7 +1,6 @@ require("dotenv").config(); const Streamyfin = require('./client'); -const { Client, GatewayIntentBits, REST, Routes, ChannelType, MessageCollector, StringSelectMenuBuilder } = require ("discord.js"); -const axios = require ("axios"); +const { GatewayIntentBits, REST, Routes } = require ("discord.js"); const fs = require("fs"); // GitHub API base URL and repo data From 270320ca2949c3f0d0cc19a6aeb300a9d258d0d5 Mon Sep 17 00:00:00 2001 From: sldless <67599596+sldless@users.noreply.github.com> Date: Tue, 31 Dec 2024 14:28:00 -0500 Subject: [PATCH 36/43] Tested all the commands | Added Select menu to issue.js --- client.js | 4 ++-- commands/github/issue.js | 37 ++++++++++++++++++++++++++++++++----- commands/github/stats.js | 3 +++ package.json | 1 + 4 files changed, 38 insertions(+), 7 deletions(-) diff --git a/client.js b/client.js index e625e15..c91adad 100644 --- a/client.js +++ b/client.js @@ -25,10 +25,10 @@ module.exports = class Streamyfin extends Client { async fetchReleases(){ try { const response = await axios.get( - `${GITHUB_API_BASE}/repos/${REPO_OWNER}/${REPO_NAME}/releases`, + `${process.env.GITHUB_API_BASE}/repos/${process.env.REPO_OWNER}/${process.env.REPO_NAME}/releases`, { headers: { - Authorization: `token ${GITHUB_TOKEN}`, + Authorization: `token ${process.env.GITHUB_TOKEN}`, }, } ); diff --git a/commands/github/issue.js b/commands/github/issue.js index c683600..e0a1912 100644 --- a/commands/github/issue.js +++ b/commands/github/issue.js @@ -1,5 +1,5 @@ -const { SlashCommandBuilder } = require('discord.js'); -const axios = require ("axios"); +const { SlashCommandBuilder, StringSelectMenuBuilder } = require('discord.js'); +const axios = require("axios"); module.exports = { data: new SlashCommandBuilder() @@ -8,15 +8,41 @@ module.exports = { .addIntegerOption(option => option.setName('number') .setDescription('The issue number') - .setRequired(true) + .setRequired(false) ), async run(interaction) { const REPO_OWNER = process.env.REPO_OWNER; const REPO_NAME = process.env.REPO_NAME; const GITHUB_TOKEN = process.env.GITHUB_TOKEN; const issueNumber = interaction.options.getInteger("number"); + const response = await axios.get(`https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/issues`) + if (!response.data) return interaction.reply("Please provide an issue number") + let options = []; + for (const [, value] of Object.entries(response.data.slice(0, 20))) { + options.push({ label: `${value.title} - ${value.number}`, value: value.node_id, description: value.body.slice(0, 97) + '...' }) + } + let menu = new StringSelectMenuBuilder() + .setCustomId(`IssueList_${interaction.user.id}`) + .setPlaceholder("Select a choice!") + .setMinValues(1) + .setMaxValues(1) + .addOptions(options); + let msg = await interaction.reply({ content: "Please select an issue!", ephemeral: false, components: [{ type: 1, components: [menu] }] }); + let filter = (msg) => interaction.user.id === msg.user.id; + let collector = msg.createMessageComponentCollector({ filter, max: 1, errors: ["time"], time: 120000 }); + + collector.on("collect", (interaction) => { + let issue = response.data.find(issue => issue.node_id === interaction.values[0]); + if (issue) { + interaction.update({ content: `πŸ”— **Issue #${issue.number}: ${issue.title}**\n${issue.html_url}` }); + } else { + interaction.update({content: "An error occurred. The issue was not found."}) + } + }) + if(issueNumber) { try { + const response = await axios.get( `https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/issues/${issueNumber}`, { @@ -33,5 +59,6 @@ module.exports = { } catch (error) { await interaction.reply("❌ Issue not found or an error occurred."); } - }, -}; \ No newline at end of file + } +} +} \ No newline at end of file diff --git a/commands/github/stats.js b/commands/github/stats.js index c65b5e6..6c79572 100644 --- a/commands/github/stats.js +++ b/commands/github/stats.js @@ -72,6 +72,9 @@ module.exports = { color: 0x6A0DAD, title: "πŸ“ˆ Contribution Overview", description: mapped, + thumbnail: { + url: repoResponse.data.organization.avatar_url + }, fields: [ { name: "⭐ Star Count", diff --git a/package.json b/package.json index 5490921..bc80704 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "1.0.0", "main": "index.js", "scripts": { + "start": "node index.js", "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], From 125a98a7fcc2d04640536aa24937449d056b3bda Mon Sep 17 00:00:00 2001 From: SimplyJanDE <78982850+SimplyJanDE@users.noreply.github.com> Date: Thu, 2 Jan 2025 09:41:18 +0100 Subject: [PATCH 37/43] feat: add piracy command and improve bot responses - Add new `/piracy` command to handle piracy-related messages - changed `/testflight`command to `/beta`to explain new beta joining procedure --- commands/bot/beta.js | 12 ++++++++++++ commands/bot/piracy.js | 11 +++++++++++ commands/bot/testflight.js | 12 ------------ 3 files changed, 23 insertions(+), 12 deletions(-) create mode 100644 commands/bot/beta.js create mode 100644 commands/bot/piracy.js delete mode 100644 commands/bot/testflight.js diff --git a/commands/bot/beta.js b/commands/bot/beta.js new file mode 100644 index 0000000..3b3268f --- /dev/null +++ b/commands/bot/beta.js @@ -0,0 +1,12 @@ +const { SlashCommandBuilder } = require('discord.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('beta') + .setDescription('Explains how to join the Streamyfin Beta program.'), + async run(interaction) { + await interaction.reply( + `To access the Streamyfin beta, you need to subscribe to the Member tier (or higher) on [Patreon](https://www.patreon.com/streamyfin). This will give you immediate access to the ⁠πŸ§ͺ-public-beta channel on Discord and weΒ΄ll know that you have subscribed. This is where we will post APKs and IPAs. This won't give automatic access to the TestFlight however, so you need to send <@${interaction.client.userId}> a DM with the email you use for Apple so that i can manually add you.` + ) + }, +}; \ No newline at end of file diff --git a/commands/bot/piracy.js b/commands/bot/piracy.js new file mode 100644 index 0000000..22a38ad --- /dev/null +++ b/commands/bot/piracy.js @@ -0,0 +1,11 @@ +const { SlashCommandBuilder } = require('discord.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('piracy') + .setDescription('How we handle piracy related messages on this discord server.'), + + async run(interaction) { + await interaction.reply("πŸ΄β€β˜ οΈ Discussions related to piracy or content acquisition are strictly prohibited on this server. This rule exists to ensure we avoid legal trouble and protect the future of this project, as well as its reputation."); + }, +}; \ No newline at end of file diff --git a/commands/bot/testflight.js b/commands/bot/testflight.js deleted file mode 100644 index a856bb0..0000000 --- a/commands/bot/testflight.js +++ /dev/null @@ -1,12 +0,0 @@ -const { SlashCommandBuilder } = require('discord.js'); - -module.exports = { - data: new SlashCommandBuilder() - .setName('testflight') - .setDescription('Explains how to join the Streamyfin Testflight.'), - async run(interaction) { - await interaction.reply( - `Currently, Streamyfin Testflight is full. However, you can send a private message to <@${interaction.client.userId}> with your email address and he will add you to the Testflight beta group manually.` - ); - }, -}; \ No newline at end of file From 8f826693b0ba50f6663b23b0af3a1665f3a5a609 Mon Sep 17 00:00:00 2001 From: SimplyJanDE <78982850+SimplyJanDE@users.noreply.github.com> Date: Thu, 2 Jan 2025 10:08:59 +0100 Subject: [PATCH 38/43] fix: command registration --- index.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index 3be71ce..700e181 100644 --- a/index.js +++ b/index.js @@ -76,7 +76,7 @@ const registerCommands = async () => { description: "Get a list of available commands.", }, { - name: "testflight", + name: "beta", description: "Explains how to join the Streamyfin Testflight.", }, { @@ -129,6 +129,10 @@ const registerCommands = async () => { name:"stats", description: "Shows how to support the Streamyfin project." }, + { + name:"piracy", + description: "Shows how we handle piracy related messages on this discord server." + } ]; const rest = new REST({ version: "10" }).setToken(process.env.DISCORD_TOKEN); From 68c7dffec988bae3ac92e94f46b0bf32fcce6bd5 Mon Sep 17 00:00:00 2001 From: SimplyJanDE <78982850+SimplyJanDE@users.noreply.github.com> Date: Thu, 2 Jan 2025 15:30:21 +0100 Subject: [PATCH 39/43] fix: Corrected the GitHub API URL and headers in fetchReleases method. --- client.js | 4 +- commands/bot/beta.js | 2 +- commands/github/issue.js | 36 +++- commands/github/stats.js | 90 ++++---- index.js | 440 ++++----------------------------------- package.json | 1 + 6 files changed, 118 insertions(+), 455 deletions(-) diff --git a/client.js b/client.js index e625e15..c91adad 100644 --- a/client.js +++ b/client.js @@ -25,10 +25,10 @@ module.exports = class Streamyfin extends Client { async fetchReleases(){ try { const response = await axios.get( - `${GITHUB_API_BASE}/repos/${REPO_OWNER}/${REPO_NAME}/releases`, + `${process.env.GITHUB_API_BASE}/repos/${process.env.REPO_OWNER}/${process.env.REPO_NAME}/releases`, { headers: { - Authorization: `token ${GITHUB_TOKEN}`, + Authorization: `token ${process.env.GITHUB_TOKEN}`, }, } ); diff --git a/commands/bot/beta.js b/commands/bot/beta.js index 3b3268f..b6e3b47 100644 --- a/commands/bot/beta.js +++ b/commands/bot/beta.js @@ -6,7 +6,7 @@ module.exports = { .setDescription('Explains how to join the Streamyfin Beta program.'), async run(interaction) { await interaction.reply( - `To access the Streamyfin beta, you need to subscribe to the Member tier (or higher) on [Patreon](https://www.patreon.com/streamyfin). This will give you immediate access to the ⁠πŸ§ͺ-public-beta channel on Discord and weΒ΄ll know that you have subscribed. This is where we will post APKs and IPAs. This won't give automatic access to the TestFlight however, so you need to send <@${interaction.client.userId}> a DM with the email you use for Apple so that i can manually add you.` + `To access the Streamyfin beta, you need to subscribe to the Member tier (or higher) on [Patreon](https://www.patreon.com/streamyfin). This will give you immediate access to the ⁠#πŸ§ͺ-public-beta channel on Discord and weΒ΄ll know that you have subscribed. This is where we will post APKs and IPAs. This won't give automatic access to the TestFlight however, so you need to send <@${interaction.client.userId}> a DM with the email you use for Apple so that i can manually add you.` ) }, }; \ No newline at end of file diff --git a/commands/github/issue.js b/commands/github/issue.js index 438a9a1..e0a1912 100644 --- a/commands/github/issue.js +++ b/commands/github/issue.js @@ -1,4 +1,5 @@ -const { SlashCommandBuilder } = require('discord.js'); +const { SlashCommandBuilder, StringSelectMenuBuilder } = require('discord.js'); +const axios = require("axios"); module.exports = { data: new SlashCommandBuilder() @@ -7,15 +8,41 @@ module.exports = { .addIntegerOption(option => option.setName('number') .setDescription('The issue number') - .setRequired(true) + .setRequired(false) ), async run(interaction) { const REPO_OWNER = process.env.REPO_OWNER; const REPO_NAME = process.env.REPO_NAME; const GITHUB_TOKEN = process.env.GITHUB_TOKEN; const issueNumber = interaction.options.getInteger("number"); + const response = await axios.get(`https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/issues`) + if (!response.data) return interaction.reply("Please provide an issue number") + let options = []; + for (const [, value] of Object.entries(response.data.slice(0, 20))) { + options.push({ label: `${value.title} - ${value.number}`, value: value.node_id, description: value.body.slice(0, 97) + '...' }) + } + let menu = new StringSelectMenuBuilder() + .setCustomId(`IssueList_${interaction.user.id}`) + .setPlaceholder("Select a choice!") + .setMinValues(1) + .setMaxValues(1) + .addOptions(options); + let msg = await interaction.reply({ content: "Please select an issue!", ephemeral: false, components: [{ type: 1, components: [menu] }] }); + let filter = (msg) => interaction.user.id === msg.user.id; + let collector = msg.createMessageComponentCollector({ filter, max: 1, errors: ["time"], time: 120000 }); + + collector.on("collect", (interaction) => { + let issue = response.data.find(issue => issue.node_id === interaction.values[0]); + if (issue) { + interaction.update({ content: `πŸ”— **Issue #${issue.number}: ${issue.title}**\n${issue.html_url}` }); + } else { + interaction.update({content: "An error occurred. The issue was not found."}) + } + }) + if(issueNumber) { try { + const response = await axios.get( `https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/issues/${issueNumber}`, { @@ -32,5 +59,6 @@ module.exports = { } catch (error) { await interaction.reply("❌ Issue not found or an error occurred."); } - }, -}; \ No newline at end of file + } +} +} \ No newline at end of file diff --git a/commands/github/stats.js b/commands/github/stats.js index 3d2f099..f02f16b 100644 --- a/commands/github/stats.js +++ b/commands/github/stats.js @@ -1,4 +1,5 @@ -const { SlashCommandBuilder } = require('discord.js'); +const { SlashCommandBuilder, StringSelectMenuBuilder } = require('discord.js'); +const axios = require ("axios"); module.exports = { data: new SlashCommandBuilder() @@ -6,7 +7,7 @@ module.exports = { .setDescription('Shows how to support the Streamyfin project.'), async run(interaction) { - const leaderboard = await fetchStats(); + const leaderboard = await interaction.client.fetchStats(); if (!leaderboard || leaderboard.length === 0) { await interaction.reply({ content: "❌ No data available", ephemeral: true }); return; @@ -15,27 +16,45 @@ module.exports = { const repoResponse = await axios.get("https://api.github.com/repos/streamyfin/streamyfin"); const starCount = repoResponse.data.stargazers_count; - - let embed = { + let embed = { + title: "Streamyfin's info", color: 0x6A0DAD, - title: "πŸ“ˆ Contribution Overview", - description: mapped, + description: repoResponse.data.description, + thumbnail: { + url: repoResponse.data.organization.avatar_url + }, fields: [ { - name: "⭐ Star Count", - value: `The repository has **${starCount}** stars.`, + name: "Forks", + value: repoResponse.data.forks_count.toLocaleString(), + inline: true + }, + { + name: "Watchers", + value: repoResponse.data.watchers.toLocaleString(), + inline: true, + }, + { + name: "Stars", + value: starCount, + inline: true, + }, + { + name: "Language", + value: repoResponse.data.language, + inline: true, + }, + { + name: "License", + value: repoResponse.data.license.name, inline: true, } - ], - timestamp: new Date(), - footer: { - text: `Star Count: ${starCount}`, - }, - }; + ] + } let options = [ { - label: "Information", - value: "Information", + label: "πŸ“ˆ Contribution Overview", + value: "Contribution", description: "Get information about the repo!" } ] @@ -50,41 +69,26 @@ module.exports = { let collector = msg.createMessageComponentCollector({ filter, max: 1, errors: ["time"], time: 120000 }); collector.on("collect", (interaction) => { embed = { - title: "Streamyfin's info", color: 0x6A0DAD, - description: repoResponse.data.description, + title: "πŸ“ˆ Contribution Overview", + description: mapped, thumbnail: { url: repoResponse.data.organization.avatar_url }, fields: [ { - name: "Forks", - value: repoResponse.data.forks_count.toLocaleString(), - inline: true - }, - { - name: "Watchers", - value: repoResponse.data.watchers.toLocaleString(), - inline: true, - }, - { - name: "Stars", - value: starCount, - inline: true, - }, - { - name: "Language", - value: repoResponse.data.language, - inline: true, - }, - { - name: "License", - value: repoResponse.data.license.name, + name: "⭐ Star Count", + value: `The repository has **${starCount}** stars.`, inline: true, } - ] - } + ], + timestamp: new Date(), + footer: { + text: `Star Count: ${starCount}`, + }, + }; interaction.update({ embeds: [embed] }); }) }, -}; \ No newline at end of file +}; + \ No newline at end of file diff --git a/index.js b/index.js index 700e181..ec85f7e 100644 --- a/index.js +++ b/index.js @@ -1,146 +1,54 @@ require("dotenv").config(); -const { Client, GatewayIntentBits, REST, Routes, ChannelType, MessageCollector } = require ("discord.js"); +const Streamyfin = require('./client'); +const { Client, GatewayIntentBits, REST, Routes, ChannelType, MessageCollector, StringSelectMenuBuilder } = require ("discord.js"); const axios = require ("axios"); +const fs = require("fs"); // GitHub API base URL and repo data -const GITHUB_API_BASE = "https://api.github.com"; -const REPO_OWNER = process.env.REPO_OWNER; -const REPO_NAME = process.env.REPO_NAME; const GITHUB_TOKEN = process.env.GITHUB_TOKEN; -const userId = '398161771476549654'; - -let commands = []; // Global commands array - -// Function to fetch releases from GitHub -const fetchReleases = async () => { - try { - const response = await axios.get( - `${GITHUB_API_BASE}/repos/${REPO_OWNER}/${REPO_NAME}/releases`, - { - headers: { - Authorization: `token ${GITHUB_TOKEN}`, - }, - } - ); - - const releases = response.data - .slice(0, 2) // Fetch the latest 2 releases - .map((release) => ({ name: release.name, value: release.name })); - - releases.push({ name: "Older", value: "Older" }); // Add "Older" as an option - - return releases; - } catch (error) { - console.error("Error fetching releases:", error); - return [ - { name: "0.22.0", value: "0.22.0" }, // Fallback data - { name: "0.21.0", value: "0.21.0" }, - { name: "Older", value: "Older" }, - ]; - } -}; +const tempCommands = [] // Initialize Discord client -const client = new Client({ - intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent], +const client = new Streamyfin({ + intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent], +}); + +fs.readdirSync("./commands/").forEach(dir => { + const files = fs.readdirSync(`./commands/${dir}/`).filter(file => file.endsWith(".js")); + for (let file of files) { + let props = require(`./commands/${dir}/${file}`); + client.commands.set(props.data.name, props); + tempCommands.push(props.data) + console.log(`[COMMAND] => Loaded ${file} `); + } }); -// Register slash commands +client.on("interactionCreate", async (interaction) => { + if (!interaction.isCommand()) return; + + const command = client.commands.get(interaction.commandName); + if (!command) return; + + try { + await command.run(interaction); + } catch (error) { + console.error(error); + await interaction.reply({ + content: 'There was an error while executing this command!', + ephemeral: true, + }); + } +}) const registerCommands = async () => { - await fetchReleases(); - - commands = [ - { - name: "roadmap", - description: "Get the link to the GitHub roadmap.", - }, - { - name: "issue", - description: "Get details about a specific issue from GitHub.", - options: [ - { - name: "number", - type: 4, // Integer - description: "The issue number", - required: true, - }, - ], - }, - { - name: "createissue", - description: "Create a new issue on GitHub.", - }, - { - name: "help", - description: "Get a list of available commands.", - }, - { - name: "beta", - description: "Explains how to join the Streamyfin Testflight.", - }, - { - name: "repo", - description: "Get the link to the GitHub repository." - }, - { - name: "closeissue", - description: "Close an issue on GitHub and lock the thread.", - options: [ - { - name: "state", - type: 3, // String - description: "The state to set for the GitHub issue (e.g., open, closed).", - required: true, - choices: [ - { name: "Open", value: "open" }, - { name: "Closed", value: "closed" }, - ], - }, - { - name: "state_reason", - type: 3, // String - description: "The reason for closing the GitHub issue.", - required: true, - choices: [ - { name: "Completed", value: "completed" }, - { name: "Not Planned", value: "not_planned" }, - ], - }, - ], - }, - { - name: "featurerequest", - description: "Request a new feature for Streamyfin.", - options: [ - { - name: "description", - type: 3, // String - description: "A short description of the feature request.", - required: true, - }, - ], - }, - { - name:"donate", - description: "Shows how to support the Streamyfin project." - }, - { - name:"stats", - description: "Shows how to support the Streamyfin project." - }, - { - name:"piracy", - description: "Shows how we handle piracy related messages on this discord server." - } - ]; + if (GITHUB_TOKEN) await client.fetchReleases(); const rest = new REST({ version: "10" }).setToken(process.env.DISCORD_TOKEN); try { console.log("Registering slash commands..."); await rest.put(Routes.applicationCommands(process.env.CLIENT_ID), { - body: commands, + body: tempCommands, }); console.log("Slash commands registered successfully!"); } catch (error) { @@ -148,283 +56,5 @@ const registerCommands = async () => { } }; -// Event when the bot is ready -client.once("ready", () => { - console.log(`Logged in as ${client.user.tag}!`); -}); - -// Handle slash command interactions -client.on("interactionCreate", async (interaction) => { - if (!interaction.isCommand()) return; - - const { commandName, options } = interaction; - - if (commandName === "repo") { - await interaction.reply( - "πŸ“‘ Here is our GitHub repository: https://github.com/fredrikburmester/streamyfin" - ); - } - - if (commandName === "roadmap") { - await interaction.reply( - "πŸ“Œ Here is our Roadmap: https://github.com/users/fredrikburmester/projects/5/views/8" - ); - } - - if (commandName === "help") { - const commandList = commands - .map((cmd) => `**/${cmd.name}**: ${cmd.description}`) - .join("\n"); - - await interaction.reply({ - content: `Available commands:\n${commandList}`, - ephemeral: false, - }); - } - - if (commandName === "issue") { - const issueNumber = options.getInteger("number"); - - try { - const response = await axios.get( - `${GITHUB_API_BASE}/repos/${REPO_OWNER}/${REPO_NAME}/issues/${issueNumber}`, - { - headers: { - Authorization: `token ${GITHUB_TOKEN}`, - }, - } - ); - - const issue = response.data; - await interaction.reply( - `πŸ”— **Issue #${issue.number}: ${issue.title}**\n${issue.html_url}` - ); - } catch (error) { - await interaction.reply("❌ Issue not found or an error occurred."); - } - } - - if (commandName === "testflight") { - await interaction.reply( - `Currently, Streamyfin Testflight is full. However, you can send a private message to <@${userId}> with your email address and he will add you to the Testflight beta group manually.` - ); - } - - if (commandName === "createissue") { - // Start by creating a private thread in forum - const forumChannelId= process.env.FORUM_CHANNEL_ID; - const forumChannel = interaction.guild.channels.cache.get(forumChannelId); - - if (!forumChannel || forumChannel.type !== ChannelType.GuildForum) { - await interaction.reply({ - content: "❌ Forum channel not found or is not a forum channel.", - ephemeral: true, - }); - return; - } - - const thread = await forumChannel.threads.create({ - name: `Issue Report by ${interaction.user.username}`, - message: { - content: `Hello ${interaction.user.username}, let's collect the details for your issue report!`, - }, - autoArchiveDuration: 60, // Auto-archive after 1 hour - type: ChannelType.PrivateThread, - reason: "Collecting issue details", - }); - - await interaction.reply({ - content: `βœ… Forum thread created, please fill out the issue report: [${thread.name}](https://discord.com/channels/${interaction.guild.id}/${forumChannelId}/${thread.id})`, - ephemeral: false, - }); - - // Define the questions to ask the user - const questions = [ - { key: "title", question: "Describe your issue in a few words." }, - { key: "description", question: "What happened? What did you expect to happen?" }, - { key: "steps", question: "How can this issue be reproduced? (step-by-step)" }, - { key: "device", question: "What device and operating system are you using?" }, - { key: "version", question: "What is the affected Streamyfin version?" }, - { key: "screenshots", question: "Please provide any screenshots that might help us reproduce the issue (optional), or type 'none'.", allowUploads: true }, - ]; - - const collectedData = {}; // Store user responses here - const uploadedFiles = []; // Store uploaded files here - // Helper function to ask questions - const askQuestion = async (questionIndex = 0) => { - if (questionIndex >= questions.length) { - // All questions have been asked - const screenshotsText = uploadedFiles.length - ? uploadedFiles.map((file) => file.url).join("\n") - : "No screenshots provided"; - - const body = ` -### What happened? -${collectedData.description} - -### Reproduction steps -${collectedData.steps} - -### Device and operating system -${collectedData.device} - -### Version -${collectedData.version} - -### Screenshots -${screenshotsText} -`; - - try { - const issueResponse = await axios.post( - `${GITHUB_API_BASE}/repos/${REPO_OWNER}/${REPO_NAME}/issues`, - { - title: `[Bug]: ${collectedData.title} reported via Discord by [${interaction.user.username}]`, - body: body, - labels: ["❌ bug"], - assignees: ["fredrikburmester"], - }, - { - headers: { - Authorization: `token ${GITHUB_TOKEN}`, - }, - } - ); - - await thread.send(`βœ… Issue created successfully: ${issueResponse.data.html_url}`); - await thread.setLocked(true, "Issue details collected and sent to GitHub."); - } catch (error) { - console.error("Error creating issue:", error); - await thread.send("❌ Failed to create the issue. Please try again."); - } - return; - } - - // Ask the next question - const question = questions[questionIndex]; - await thread.send(question.question); - - // Set up a message collector to get the user's response - const collector = new MessageCollector(thread, { - filter: (msg) => msg.author.id === interaction.user.id, - max: 1, // Collect only one message - time: 300000, // Timeout after 5 minutes - }); - - collector.on("collect", async (msg) => { - if (question.allowUploads && msg.attachments.size > 0) { - msg.attachments.forEach((attachment) => { - uploadedFiles.push({ - name: attachment.filename, - url: attachment.url, - }); - }); - collectedData[question.key] = "Screenshots uploaded"; - } else { - collectedData[question.key] = msg.content; - - if (question.key ==="title") { - try { - await thread.setName(`Issue ${collectedData.title} reported by ${interaction.user.username}`); - } catch (error) { - console.error("Error setting thread name:", error); - await thread.send("❌ Failed to set the thread name. Please try again."); - } - } - } - askQuestion(questionIndex + 1); - }); - - collector.on("end", (collected, reason) => { - if (reason === "time") { - thread.send("❌ You did not respond in time. Please run the command again if you'd like to create an issue."); - } - }); - }; - - // Start asking questions - askQuestion(); - } - - if (commandName === "closeissue") { - const allowedRoles = ["Developer", "Administrator"]; - const state = options.getString("state"); - const stateReason = options.getString("state_reason"); - const thread = interaction.channel; - - // Check if the user has the required role - const memberRoles = interaction.member.roles.cache.map((role) => role.name); - if (!memberRoles.some((role) => allowedRoles.includes(role))) { - await interaction.reply({ content: "❌ You do not have permission to use this command.", ephemeral: true }); - return; - } - - // Check if the command is executed in a forum thread - if (thread.type !== ChannelType.PublicThread && thread.type !== ChannelType.PrivateThread) { - await interaction.reply({ content: "❌ This command can only be used in a forum thread.", ephemeral: true }); - return; - } - - try { - const messages = await thread.messages.fetch({ limit: 100 }); - const githubLinkMessage = messages.find( - (msg) => msg.content.includes("https://github.com") && msg.content.includes("/issues/") - ); - - if (!githubLinkMessage) { - await interaction.reply({ content: "❌ No GitHub link found in the thread.", ephemeral: true }); - return; - } - - const issueUrlMatch = githubLinkMessage.content.match(/\/issues\/(\d+)/); - if (!issueUrlMatch) { - await interaction.reply({ content: "❌ Invalid GitHub link in the thread.", ephemeral: true }); - return; - } - - const issueNumber = issueUrlMatch[1]; - - await axios.patch( - `${GITHUB_API_BASE}/repos/${REPO_OWNER}/${REPO_NAME}/issues/${issueNumber}`, - { state, state_reason: stateReason }, - { headers: { Authorization: `token ${GITHUB_TOKEN}` } } - ); - - await thread.setLocked(true, "Thread closed by developer."); - await thread.send(`βœ… This issue has been resolved and the GitHub issue is now "${state}" with reason "${stateReason}".`); - await interaction.reply({ content: "βœ… Issue closed successfully.", ephemeral: true }); - } catch (error) { - console.error("Error closing issue:", error); - await interaction.reply({ content: "❌ Failed to close the issue. Please try again.", ephemeral: true }); - } - } - - - - if (commandName === "donate") { - await interaction.reply({ - content: `🎁 Thank you for supporting our work and sharing your experiences! While many contributors are involved, the majority of the work is done by <@${userId}>. The best way to show your support is by buying him a coffee: https://buymeacoffee.com/fredrikbur3`, - }); - } - - if (commandName ==="featurerequest") { - const description = options.getString("description"); - const targetChannel = client.channels.cache.get('1273278866105831424'); - if (!targetChannel) { - await interaction.reply({ content:'❌ Target channel not found.', ephemeral: true}); - return; - } - - const thread = await targetChannel.threads.create({ - name: `Feature: ${description} requested by ${interaction.user.username}`, - reason: 'User requested a feature', - }); - - await thread.send({content: `πŸŽ‰ Thank you for your feature request! Feel free to discuss this feature here!`}); - await interaction.reply({content: `βœ… Your feature request has been submitted and a discussion thread has been created: [${thread.name}](https://discord.com/channels/${interaction.guild.id}/${targetChannel.id}/${thread.id})`, ephemeral: true}); - } -}); - -// Register commands and start the bot registerCommands(); -client.login(process.env.DISCORD_TOKEN); \ No newline at end of file +client.login(process.env.DISCORD_TOKEN); diff --git a/package.json b/package.json index 5490921..bc80704 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "1.0.0", "main": "index.js", "scripts": { + "start": "node index.js", "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], From 50b96857d55c6ff366ea2538ce14842016ba385d Mon Sep 17 00:00:00 2001 From: retardgerman <78982850+retardgerman@users.noreply.github.com> Date: Thu, 2 Jan 2025 15:40:34 +0100 Subject: [PATCH 40/43] fix: add missing GitHub API URL --- .env.example | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index f88993e..85e44ef 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,5 @@ DISCORD_TOKEN=your_discord_bot_token CLIENT_ID=your_discord_bot_client_id GITHUB_TOKEN=your_github_token -FORUM_CHANNEL_ID=your_forum_channel_id \ No newline at end of file +FORUM_CHANNEL_ID=your_forum_channel_id +GITHUB_API_BASE=https://api.github.com From 82d838d5c397e5c205f82e046f7e3b171f1f77d4 Mon Sep 17 00:00:00 2001 From: SimplyJanDE <78982850+SimplyJanDE@users.noreply.github.com> Date: Thu, 2 Jan 2025 20:34:32 +0100 Subject: [PATCH 41/43] fix: typo --- commands/bot/beta.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands/bot/beta.js b/commands/bot/beta.js index b6e3b47..a491487 100644 --- a/commands/bot/beta.js +++ b/commands/bot/beta.js @@ -6,7 +6,7 @@ module.exports = { .setDescription('Explains how to join the Streamyfin Beta program.'), async run(interaction) { await interaction.reply( - `To access the Streamyfin beta, you need to subscribe to the Member tier (or higher) on [Patreon](https://www.patreon.com/streamyfin). This will give you immediate access to the ⁠#πŸ§ͺ-public-beta channel on Discord and weΒ΄ll know that you have subscribed. This is where we will post APKs and IPAs. This won't give automatic access to the TestFlight however, so you need to send <@${interaction.client.userId}> a DM with the email you use for Apple so that i can manually add you.` + `To access the Streamyfin beta, you need to subscribe to the Member tier (or higher) on [Patreon](https://www.patreon.com/streamyfin). This will give you immediate access to the ⁠#πŸ§ͺ-public-beta channel on Discord and weΒ΄ll know that you have subscribed. This is where we will post APKs and IPAs. This won't give automatic access to the TestFlight however, so you need to send <@${interaction.client.userId}> a DM with the email you use for Apple so that he can manually add you.` ) }, }; \ No newline at end of file From 0af005d720cb34f62cd7c9311fe79ffd4d0fdf6b Mon Sep 17 00:00:00 2001 From: sldless <67599596+sldless@users.noreply.github.com> Date: Thu, 2 Jan 2025 17:27:55 -0500 Subject: [PATCH 42/43] feat: enhance GitHub issue retrieval and add activity status --- commands/github/issue.js | 54 +++++++++++++++++++++------------------- index.js | 3 +++ 2 files changed, 31 insertions(+), 26 deletions(-) diff --git a/commands/github/issue.js b/commands/github/issue.js index e0a1912..4cad401 100644 --- a/commands/github/issue.js +++ b/commands/github/issue.js @@ -15,11 +15,34 @@ module.exports = { const REPO_NAME = process.env.REPO_NAME; const GITHUB_TOKEN = process.env.GITHUB_TOKEN; const issueNumber = interaction.options.getInteger("number"); - const response = await axios.get(`https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/issues`) - if (!response.data) return interaction.reply("Please provide an issue number") + + if (issueNumber) { + try { + const response = await axios.get( + `https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/issues/${issueNumber}`, + { + headers: { + Authorization: `token ${GITHUB_TOKEN}`, + }, + } + ); + + const issue = response.data; + await interaction.reply( + `πŸ”— **Issue #${issue.number}: ${issue.title}**\n${issue.html_url}` + ); + } catch (error) { + await interaction.reply("❌ Issue not found or an error occurred."); + } + return; + } + + const response = await axios.get(`https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/issues`); + if (!response.data) return interaction.reply("Please provide an issue number"); + let options = []; for (const [, value] of Object.entries(response.data.slice(0, 20))) { - options.push({ label: `${value.title} - ${value.number}`, value: value.node_id, description: value.body.slice(0, 97) + '...' }) + options.push({ label: `${value.title} - ${value.number}`, value: value.node_id, description: value.body.slice(0, 97) + '...' }); } let menu = new StringSelectMenuBuilder() .setCustomId(`IssueList_${interaction.user.id}`) @@ -29,36 +52,15 @@ module.exports = { .addOptions(options); let msg = await interaction.reply({ content: "Please select an issue!", ephemeral: false, components: [{ type: 1, components: [menu] }] }); let filter = (msg) => interaction.user.id === msg.user.id; - let collector = msg.createMessageComponentCollector({ filter, max: 1, errors: ["time"], time: 120000 }); + let collector = msg.createMessageComponentCollector({ filter, errors: ["time"], time: 120000 }); collector.on("collect", (interaction) => { let issue = response.data.find(issue => issue.node_id === interaction.values[0]); if (issue) { interaction.update({ content: `πŸ”— **Issue #${issue.number}: ${issue.title}**\n${issue.html_url}` }); } else { - interaction.update({content: "An error occurred. The issue was not found."}) + interaction.update({ content: "An error occurred. The issue was not found." }) } }) - - if(issueNumber) { - try { - - const response = await axios.get( - `https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/issues/${issueNumber}`, - { - headers: { - Authorization: `token ${GITHUB_TOKEN}`, - }, - } - ); - - const issue = response.data; - await interaction.reply( - `πŸ”— **Issue #${issue.number}: ${issue.title}**\n${issue.html_url}` - ); - } catch (error) { - await interaction.reply("❌ Issue not found or an error occurred."); - } } -} } \ No newline at end of file diff --git a/index.js b/index.js index 706d9a1..cbe8801 100644 --- a/index.js +++ b/index.js @@ -23,6 +23,9 @@ fs.readdirSync("./commands/").forEach(dir => { } }); +client.on("ready", () => { + client.user.setActivity("over Streamyfin's issues πŸ‘€", { type: 3 }); +}) client.on("interactionCreate", async (interaction) => { if (!interaction.isCommand()) return; From 26b801d73284311b11e1eb9edfe2b5ae6b5b21f7 Mon Sep 17 00:00:00 2001 From: sldless <67599596+sldless@users.noreply.github.com> Date: Mon, 6 Jan 2025 13:42:50 -0500 Subject: [PATCH 43/43] refactor: update GitHub API integration to use instance variables for repository details --- client.js | 7 ++- commands/bot/beta.js | 3 +- commands/github/closeissue.js | 13 +++--- commands/github/createissue.js | 72 ++++++++----------------------- commands/github/featurerequest.js | 64 +++++++++++++++++++++++++-- commands/github/issue.js | 9 ++-- commands/github/roadmap.js | 2 +- index.js | 4 +- 8 files changed, 96 insertions(+), 78 deletions(-) diff --git a/client.js b/client.js index c91adad..7f743b0 100644 --- a/client.js +++ b/client.js @@ -7,6 +7,9 @@ module.exports = class Streamyfin extends Client { this.commands = new Collection(); this.userId = '398161771476549654'; + this.repoOwner = process.env.REPO_OWNER; + this.repoName = process.env.REPO_NAME; + this.githubToken = process.env.GITHUB_TOKEN; } @@ -25,10 +28,10 @@ module.exports = class Streamyfin extends Client { async fetchReleases(){ try { const response = await axios.get( - `${process.env.GITHUB_API_BASE}/repos/${process.env.REPO_OWNER}/${process.env.REPO_NAME}/releases`, + `${process.env.GITHUB_API_BASE}/repos/${this.repoOwner}/${this.repoName}/releases`, { headers: { - Authorization: `token ${process.env.GITHUB_TOKEN}`, + Authorization: `token ${this.githubToken}`, }, } ); diff --git a/commands/bot/beta.js b/commands/bot/beta.js index aa5b943..a491487 100644 --- a/commands/bot/beta.js +++ b/commands/bot/beta.js @@ -6,8 +6,7 @@ module.exports = { .setDescription('Explains how to join the Streamyfin Beta program.'), async run(interaction) { await interaction.reply( - `To access the Streamyfin beta, you need to subscribe to the Member tier (or higher) on [Patreon](https://www.patreon.com/streamyfin). This will give you immediate access to the beta channels on Discord and weΒ΄ll know that you have subscribed. This is where we will post APKs and IPAs. This won't give automatic access to the TestFlight however, so you need to send <@${interaction.client.userId}> a DM with the email you use for Apple so that he can manually add you.` - + `To access the Streamyfin beta, you need to subscribe to the Member tier (or higher) on [Patreon](https://www.patreon.com/streamyfin). This will give you immediate access to the ⁠#πŸ§ͺ-public-beta channel on Discord and weΒ΄ll know that you have subscribed. This is where we will post APKs and IPAs. This won't give automatic access to the TestFlight however, so you need to send <@${interaction.client.userId}> a DM with the email you use for Apple so that he can manually add you.` ) }, }; \ No newline at end of file diff --git a/commands/github/closeissue.js b/commands/github/closeissue.js index dd6ba4a..eee31e3 100644 --- a/commands/github/closeissue.js +++ b/commands/github/closeissue.js @@ -25,9 +25,6 @@ module.exports = { ), async run(interaction) { const GITHUB_API_BASE = "https://api.github.com"; - const REPO_OWNER = process.env.REPO_OWNER; - const REPO_NAME = process.env.REPO_NAME; - const GITHUB_TOKEN = process.env.GITHUB_TOKEN; const allowedRoles = ["Developer", "Administrator"]; const state = interaction.options.getString("state"); const stateReason = interaction.options.getString("state_reason"); @@ -66,13 +63,17 @@ module.exports = { const issueNumber = issueUrlMatch[1]; await axios.patch( - `${GITHUB_API_BASE}/repos/${REPO_OWNER}/${REPO_NAME}/issues/${issueNumber}`, + `${GITHUB_API_BASE}/repos/${interaction.client.repoOwner}/${interaction.client.repoName}/issues/${issueNumber}`, { state, state_reason: stateReason }, - { headers: { Authorization: `token ${GITHUB_TOKEN}` } } + { headers: { Authorization: `token ${interaction.client.githubToken}` } } ); + + await thread.setLocked(true, "Thread closed by developer."); + await thread.send(`βœ… This issue has been resolved and the GitHub issue is now "${state}" with reason "${stateReason}".`); + await interaction.reply({ content: "βœ… Issue closed successfully.", ephemeral: true }); } catch (error) { console.error("Error closing issue:", error); await interaction.reply({ content: "❌ Failed to close the issue. Please try again.", ephemeral: true }); } }, -}; +}; \ No newline at end of file diff --git a/commands/github/createissue.js b/commands/github/createissue.js index 7744a6f..c22c1a0 100644 --- a/commands/github/createissue.js +++ b/commands/github/createissue.js @@ -6,10 +6,8 @@ module.exports = { .setName('createissue') .setDescription('Create a new issue on GitHub.'), async run(interaction) { - const REPO_OWNER = process.env.REPO_OWNER; const GITHUB_API_BASE = "https://api.github.com"; - const REPO_NAME = process.env.REPO_NAME; - const GITHUB_TOKEN = process.env.GITHUB_TOKEN; + // Start by creating a private thread in forum const forumChannelId = process.env.FORUM_CHANNEL_ID; const forumChannel = interaction.guild.channels.cache.get(forumChannelId); @@ -58,58 +56,25 @@ module.exports = { : "No screenshots provided"; const body = ` - ### What happened? - ${collectedData.description} - - ### Reproduction steps - ${collectedData.steps} - - ### Device and operating system - ${collectedData.device} - - ### Version - ${collectedData.version} - - ### Screenshots - ${screenshotsText} - `; - - const notifyAndDeleteThread = async (thread) => { - try { - await thread.send("⚠️ This thread will be deleted in 1 minute because the issue has been closed on GitHub."); - setTimeout(async () => { - await thread.delete("Issue closed on GitHub."); - }, 60000); // Delete after 1 minute - } catch (error) { - console.error("Error notifying and deleting thread:", error); - } - }; - - const checkIssueClosed = async (issueNumber, thread) => { - try { - const issueResponse = await axios.get( - `${GITHUB_API_BASE}/repos/${REPO_OWNER}/${REPO_NAME}/issues/${issueNumber}`, - { - headers: { - Authorization: `token ${GITHUB_TOKEN}`, - }, - } - ); - - if (issueResponse.data.state === "closed") { - notifyAndDeleteThread(thread); - } else { - setTimeout(() => checkIssueClosed(issueNumber, thread), 60000); // Check again in 1 minute - } - } catch (error) { - console.error("Error checking issue state:", error); - setTimeout(() => checkIssueClosed(issueNumber, thread), 60000); // Check again in 1 minute - } - }; +### What happened? +${collectedData.description} + +### Reproduction steps +${collectedData.steps} + +### Device and operating system +${collectedData.device} + +### Version +${collectedData.version} + +### Screenshots +${screenshotsText} +`; try { const issueResponse = await axios.post( - `${GITHUB_API_BASE}/repos/${REPO_OWNER}/${REPO_NAME}/issues`, + `${GITHUB_API_BASE}/repos/${interaction.client.repoOwner}/${interaction.client.repoName}/issues`, { title: `[Bug]: ${collectedData.title} reported via Discord by [${interaction.user.username}]`, body: body, @@ -117,14 +82,13 @@ module.exports = { }, { headers: { - Authorization: `token ${GITHUB_TOKEN}`, + Authorization: `token ${interaction.client.githubToken}`, }, } ); await thread.send(`βœ… Issue created successfully: ${issueResponse.data.html_url}`); await thread.setLocked(true, "Issue details collected and sent to GitHub."); - checkIssueClosed(issueResponse.data.number, thread); // Start checking if the issue is closed } catch (error) { console.error("Error creating issue:", error); await thread.send("❌ Failed to create the issue. Please try again."); diff --git a/commands/github/featurerequest.js b/commands/github/featurerequest.js index a6c9cb0..a8683db 100644 --- a/commands/github/featurerequest.js +++ b/commands/github/featurerequest.js @@ -1,4 +1,5 @@ -const { SlashCommandBuilder } = require('discord.js'); +const { SlashCommandBuilder, ModalBuilder, TextInputBuilder, TextInputStyle, ButtonStyle, ButtonBuilder, ActionRowBuilder } = require('discord.js'); +const axios = require('axios'); module.exports = { data: new SlashCommandBuilder() @@ -8,10 +9,19 @@ module.exports = { option.setName('description') .setDescription('The description of the feature you want to request.') .setRequired(true)), - async run(interaction) { const description = interaction.options.getString("description"); const targetChannel = interaction.client.channels.cache.get('1273278866105831424'); + const memberRoles = interaction.member.roles.cache.map((role) => role.name); + const allowedRoles = ["Developer", "Administrator"]; + + const modal = new ModalBuilder().setCustomId('githubModal').setTitle('Github Username'); + const usernameInput = new TextInputBuilder().setCustomId('username').setLabel('Please enter your Github username').setPlaceholder('Github Username').setStyle(TextInputStyle.Short); + const button = new ButtonBuilder().setCustomId('submit').setLabel('Submit to Github').setStyle(ButtonStyle.Success); + const row = new ActionRowBuilder().addComponents(button); + + modal.addComponents(new ActionRowBuilder().addComponents(usernameInput)); + if (!targetChannel) { await interaction.reply({ content: '❌ Target channel not found.', ephemeral: true }); return; @@ -22,7 +32,53 @@ module.exports = { reason: 'User requested a feature', }); - await thread.send({ content: `πŸŽ‰ Thank you for your feature request! Feel free to discuss this feature here!` }); + await thread.send({ content: `πŸŽ‰ Thank you for your feature request! Feel free to discuss this feature here!`, components: [row] }); await interaction.reply({ content: `βœ… Your feature request has been submitted and a discussion thread has been created: [${thread.name}](https://discord.com/channels/${interaction.guild.id}/${targetChannel.id}/${thread.id})`, ephemeral: true }); + + const filter = (i) => i.user.id === interaction.user.id; + const collector = thread.createMessageComponentCollector({ filter, time: 60000 }); + + collector.on('collect', async i => { + if (i.customId === 'submit') { + if (!memberRoles.some((role) => allowedRoles.includes(role))) { + await i.reply({ content: `❌ <#${interaction.user.id}>, You do not have permission to submit this to github.`, ephemeral: true }); + } else { + await i.showModal(modal); + } + } + }); + + interaction.client.on('interactionCreate', async (modalInteraction) => { + if (!modalInteraction.isModalSubmit()) return; + if (modalInteraction.customId === 'githubModal') { + const githubUsername = modalInteraction.fields.getTextInputValue('username'); + const r = await axios.get(`https://api.github.com/users/${githubUsername}`); + if (!(r.data.login === githubUsername)) await modalInteraction.reply({ content: `❌ Github username ${githubUsername} is not valid.`, ephemeral: true }); + + try { + const response = await axios.post( + `https://api.github.com/repos/streamyfin/streamyfin/issues`, + { + title: `Feature request from Discord user ${interaction.user.username}`, + body: description, + labels: ["✨ enhancement"], + assignees: [githubUsername], + }, + { + headers: { + Authorization: `token ${interaction.client.githubToken}`, + }, + } + ); + + await interaction.reply(`βœ… Feature request created successfully: ${response.data.html_url}`); + await interaction.channel.send("πŸ”’ This channel has been locked as details have been collected and sent to GitHub."); + } catch (error) { + console.error("Error submitting feature request:", error); + await interaction.channel.send("❌ Failed to submit the feature request. Please try again."); + } + + } + }); }, -} +}; \ No newline at end of file diff --git a/commands/github/issue.js b/commands/github/issue.js index 4cad401..82f5cbf 100644 --- a/commands/github/issue.js +++ b/commands/github/issue.js @@ -11,18 +11,15 @@ module.exports = { .setRequired(false) ), async run(interaction) { - const REPO_OWNER = process.env.REPO_OWNER; - const REPO_NAME = process.env.REPO_NAME; - const GITHUB_TOKEN = process.env.GITHUB_TOKEN; const issueNumber = interaction.options.getInteger("number"); if (issueNumber) { try { const response = await axios.get( - `https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/issues/${issueNumber}`, + `https://api.github.com/repos/${interaction.client.repoOwner}/${interaction.client.repoName}/issues/${issueNumber}`, { headers: { - Authorization: `token ${GITHUB_TOKEN}`, + Authorization: `token ${interaction.client.githubToken}`, }, } ); @@ -37,7 +34,7 @@ module.exports = { return; } - const response = await axios.get(`https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/issues`); + const response = await axios.get(`https://api.github.com/repos/${interaction.client.repoOwner}/${interaction.client.repoName}/issues`); if (!response.data) return interaction.reply("Please provide an issue number"); let options = []; diff --git a/commands/github/roadmap.js b/commands/github/roadmap.js index 9440ae9..87dca67 100644 --- a/commands/github/roadmap.js +++ b/commands/github/roadmap.js @@ -6,7 +6,7 @@ module.exports = { .setDescription('Get the link to the GitHub roadmap.'), async run(interaction) { await interaction.reply( - "πŸ“Œ Here is our Roadmap: " + "πŸ“Œ Here is our Roadmap: " ); }, }; \ No newline at end of file diff --git a/index.js b/index.js index cbe8801..90f32fd 100644 --- a/index.js +++ b/index.js @@ -3,8 +3,6 @@ const Streamyfin = require('./client'); const { GatewayIntentBits, REST, Routes } = require ("discord.js"); const fs = require("fs"); -// GitHub API base URL and repo data -const GITHUB_TOKEN = process.env.GITHUB_TOKEN; const tempCommands = [] @@ -43,7 +41,7 @@ client.on("interactionCreate", async (interaction) => { } }) const registerCommands = async () => { - if (GITHUB_TOKEN) await client.fetchReleases(); + if (client.githubToken) await client.fetchReleases(); const rest = new REST({ version: "10" }).setToken(process.env.DISCORD_TOKEN);