Node.js Discord Bot: Create GitHub Issues with Slash Commands

Node.js Discord Bot: Create GitHub Issues with Slash Commands

A Discord bot that can help you create Github issues from the Discord chat via Slash Commands, let's begin.

We will be using Nodejs to build the bot while using the discord.js library. For the bot to talk to GitHub we will be using the GitHub-provided SDK Octokit. To deal with environment variables we will be using the npm package Dotenv. The Octokit SDK that will talk to GitHub needs a fetch package, for that we will be installing the package Node Fetch. We will be using two more libraries to be able to enable slash commands, @discordjs/rest and @discordjs/builders. Also, let's install another package named discord-api-types for type definitions.

That was all about installing packages. Now we will move on to some actual coding.

Create a project folder with your bot project name. And do npm init, to initialize npm.

NPM is our package manager and all the packages we talked about will be installed through npm commands.

npm install dotenv discord.js @discordjs/rest @discordjs/builders discord-api-types/v10 node-fetch octokit

The above command will install the needed packages. Now visit the package.json file which was auto-generated while we did npm init. There check to see if "main" is pointing to "index.js". Also while we are in here add the line "type": "module" to enable ES6 modules.

The finished package.json will look like below.

{
  "name": "yourbotname",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "type": "module",
}

Now we create another file named .env, this file will have the environment variables. The secrets that we don't want anyone to see.

Just because we are on the topic of secrets, and if you have plans to push the code to GitHub, never push this file .env to GitHub. For that create another file named .gitignore, so that when you do git add and git commit, the .env file is ignored.

.env

Moving on to the .env file, the file will have the following environment variables.

BOT_TOKEN=<your-bot-token-from-discord-application>
DISCORD_CLIENT_ID=<your-application-client-id>
DISCORD_GUILD_ID=<your-server-id>
GITHUB_AUTH_TOKEN=<your-github-auth-pat-token>
GITHUB_USERNAME=<your-github-username>

Before jumping in and creating the index.js file and getting going with coding. Let's figure out how to get BOT_TOKEN, DISCORD_CLIENT_ID, DISCORD_GUILD_ID, and GITHUB_AUTH_TOKEN.

First, let's dive in for the BOT_TOKEN. For that visit the Discord Developer Portal and on the Application page, click on the giant "Blurple" colored button that says New Application.

Before that if you have not turned on the Developer Mode. Please do so by clicking on the gear icon next to your username to get to the User Settings page.

There scroll a bit down while keeping an eye on the left panel items to find the one named Advanced. And then turn that Developer Mode on to become a Developer.

Now that you are a Developer and have the Developer permissions, let's continue with creating a new application.

In the pop-up appearing after clicking the New Application button while you were on the Developer Portal. Enter your dream bot project name (Discord doesn't allow the naming of bots with names containing words like discord, bot, etc.). Select the check box and then proceed by clicking the button that says Create.

On the page that opens after the bot is created. There on the General Information page, you can beautify your bot by adding a profile picture and such.

Then move your eyes through the menu items listed under General Information on the left side panel. Then click on Bot.

While on the page, turn that Public Bot option off, if you don't want people other than you using the bot. Initially, you won't see the Token listed like in the screenshot. But then you will see the Reset Token button, click on that and proceed. And there you will have your Token displayed. Remember it's a secret, a deep secret to be kept secret. Don't ever take a screenshot of that Token post it on a blog article for the world to see.

Copy the TOKEN and paste it into your .env file corresponding to BOT_TOKEN. And keep it a secret.

BOT_TOKEN=MTE1NjQzOTgxNDQ2NDM0MDAxOQ.Gixlqs.HZlLAsfqaQdECiAgQ5v7UE3uec5eoYazliDW1M

Then scroll a bit and under the Privileged Gateway Intents, turn on the option for Message Content Intent. This will allow your bot to see the chat message contents happening on your server.

Now again gaze your eyes through the left panel and click on OAuth2, which is above Bot on the side panel. Here copy CLIENT ID displayed under Client Information.

Then click on URL Generator, which will be in the dropdown that just opened while you clicked OAuth2.

On the screen, where a giant box of checkboxes with scops appears, click the checkmark for bot. Now that would have opened another giant box of checkboxes with Bot Permissions. Here select Administrator. (It is best practice to only provide the permissions that you really intend to use for security purposes.)

Now scrolling to a bit bottom, you will see a URL. That's the generated URL for you to invite your bot to your server. Copy and paste the URL into a new tab of your browser.

On the page that opens select a Server of your choice in which you intend to add the bot. Click Continue. A confirmation page opens asking if you are sure about the permissions that you are giving to this newly born bot. If you are good with it, then click Authorize.

Done, there in you get a success message of your bot being added to the server. Now if you go to your Server and check the members list you will see your bot listed.

Now let's dive back into coding, the bot, and deploying it.

In the index.js file, we will start by doing the imports of the packages we installed.

import 'dotenv/config'
import { Octokit } from 'octokit'
import fetch from 'node-fetch'
import { Client, GatewayIntentBits } from 'discord.js'
import { REST } from '@discordjs/rest'
import { Routes } from 'discord-api-types/v10'
import { SlashCommandBuilder } from '@discordjs/builders'

Then we will initialize both Discord client.

const client = new Client({
  intents: [
    GatewayIntentBits.Guilds,
    GatewayIntentBits.GuildMessages,
    GatewayIntentBits.MessageContent
  ]
})

Now to do the same for Ocktokit, that is to initialize Octkokit. We need GitHub PAT (Personal Access Token)

Log in to GitHub and then click on your profile, in the menu, scroll down and select Settings. Now in on your profile page, scroll to the bottom to find the Developer Settings link. That is on the left panel, the last item.

Now you will see a page with GitHub Apps, OAuth Apps, and Personal Access Tokens.

We know what to click here, select Personal Access tokens. In the newly opened sub-menu of items under PAT, click on Tokens (Classic).

On the page, click, Generate New Token and again go for the option of Classic. Now we have to enter a note to understand why created this token when we come back in the future. Then select an expiration for the bot from the dropdown selection. Then we are to carefully select the permissions we need for the bot to work with GitHub with this newly generated access token.

Then we click on the green button at the bottom that says Generate Toke

On the next opened screen, copy your personal access token. It's a one-time thing, you won't be able to see the token again on this page. So save it in your .env, and keep it away from spying eyes. Never share it with anyone.

BOT_TOKEN=MTE1NjQzOTgxNDQ2NDM0MDAxOQ.Gixlqs.HZlLAsfqaQdECiAgQ5v7UE3uec5eoYazliDW1M
DISCORD_CLIENT_ID=7951522907057725878
GITHUB_ACCESS_TOKEN=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

Now that we have got Personal Access Token from GitHub, let's now initialize Octokit.

const octokit = new Octokit({
  auth: process.env.GITHUB_AUTH_TOKEN,
  request: {
    fetch
  }
})

Here we are passing the token from our environment to the auth parameter on the initialization. And also we are setting fetch as our choice request handler for Octokit.

Now we will add a listener to know when our discord client is connected and ready to go ahead and listen for messages.

client.on("ready", () => {
  console.log(`Logged in as MyNewBot!`);
});

Okay, so we have our client-ready code done, which will do a console log, we have our bot token, client ID, and GitHub personal access token.

Now we need the server ID, which you can find by right-clicking on your server name. Then select the last item in the opened menu which says Copy Server ID. Now paste the server ID in the .env file in correspondence to the variable DISCORD_GUILD_ID, and then we are good to go.

We will go back to index.js file. Under the client initialization, we will create a new variable rest and initialize it with the discord rest package we installed by providing the bot token from our .env file.

const rest = new REST({ version: '10' }).setToken(process.env.BOT_TOKEN)

Then we will create our commands in a variable of an array named commands. Find more about command building in the docs provided by discord. We will have two main commands for creating an issue and another command to get all issues.

const commands = [
  new SlashCommandBuilder()
    .setName('issue')
    .setDescription('To deal with github issues')
    .addSubcommand(subcommand =>
      subcommand.setName('create')
        .setDescription('Create a new issue')
        .addStringOption(option =>
          option
            .setName('project')
            .setDescription('Project name')
            .setRequired(true)
        )
        .addStringOption(option =>
          option
            .setName('title')
            .setDescription('Issue title')
            .setRequired(true)
        )
        .addStringOption(option =>
          option
            .setName('description')
            .setDescription('Issue description')
            .setRequired(true)
        )
    )
    .addSubcommand(subcommand =>
      subcommand.setName('get')
        .setDescription('Get all issues from project')
        .addStringOption(option =>
          option
            .setName('project')
            .setDescription('Project name')
            .setRequired(true)
        )
    )
    .toJSON()
];

Now that we have our commands ready, we will write an anonymous function that will invoke at the start of the server and will send the discord server our commands.

(async () => {
  try {
    console.log('Started refreshing application (/) commands.')

    await rest.put(
      Routes.applicationGuildCommands(process.env.DISCORD_CLIENT_ID, process.env.DISCORD_GUILD_ID),
      { body: commands }
    )

    console.log('Successfully reloaded application (/) commands.')
  } catch (error) {
    console.error(error)
  }
})()

/*
The client id and guild id we have in the .env file are passed here
in the arguments along with commands we created in the value of body.
*/

Like we listened for client-ready we will listen for interactions. For that we will use interactionCreate listener and inside we will return without doing anything if the interaction.isCommand check returns false.

client.on('interactionCreate', async (interaction) => {
  if (!interaction.isCommand()) return

  const { commandName, options } = interaction

  if (commandName === 'issue') {
    const subcommand = options.getSubcommand()

    if (subcommand === 'create') {
      const projectName = options.getString('project')
      const issueTitle = options.getString('title')
      const issueDescription = options.getString('description')

      await octokit.rest.issues
        .create({
          owner: process.env.GITHUB_USERNAME,
          repo: projectName,
          title: issueTitle,
          body: issueDescription
        })
        .then(async ({ data }) => {
          await interaction.reply(
          `Issue created in ${projectName} repository with title ${issueTitle}
          Issue URL: ${data.html_url}
          `
          )
        })
        .catch((err) => {
          console.log('err::: ', err)
        })
    } else if (subcommand === 'get') {
      const projectName = options.getString('project')

      await octokit.rest.issues.listForRepo({
        owner: process.env.GITHUB_USERNAME,
        repo: projectName
      })
        .then(async ({ data }) => {
          console.log('data::: ', data)

          const issues = data.map((issue) => issue.html_url)

          await interaction.reply(
          `Fetching issues from ${projectName} repository:
          Done:
          ${issues.join('\n')}
          `
          )
        })
        .catch((err) => {
          console.log('err::: ', err)
        })
    }
  }
})

When we know it's a command and we extract the command name, options from the variable "interact" that we receive in the callback. Then we proceed with our if checks and the steps to create issues and fetch issues with the package Octokit.

That's it, we are all set. The only thing that we need to do now is let the bot client login by passing the bot token.

client.login(process.env.BOT_TOKEN)

Now deploy the bot, visit your discord server, and type / in the chat and see your commands coming alive. Select and enter the parameters. You will receive a reply from the bot telling of its new creation of an issue on your provided repository. Or the list of issues it fetches based on your provided project name.

The easiest forms of deployments are through Heroku or Railway.
Full source code: GithubDiscordBot.