Posted on 4 Comments

Create A ChatGPT Discord Bot With Slash Commands

Adding ChatGPT and OpenAI To Your Discord Server

I love writing Discord bots, my favorite one was the Discord Twitter Bot. Now that ChatGPT API is available to developers I decided to create an OpenAI ChatGPT discord bot in Node.js and share the source code with you. This bot adds a slash command to your server called /generate-prompt that takes in a prompt string and the bot returns a result using the Text Completion API.

MastaGPT Bot in action

The Source Code

This is a dockerized node.js application that uses GitHub actions to deploy the Docker container to Docker Hub and GitHub Container Registry. The index.js file loads an instance of OpenAI and Discord.js, loads the slash commands from a commands directory and registers them with Discord. It then listens for interactions i.e. a user using the slash command and then calls the generate method to use the gpt-3.5-turbo OpenAI language model to generate a response and reply to that message in Discord.

Listen To Some Hacker Music While You Code

Follow me on Spotify I make Tech Trap music

Package.json

Below is an example of how you might want your package.json file to look like.

{
  "name": "discord-gpt-bot",
  "version": "1.1.0",
  "description": "Add ChatGPT to your Discord server. Responds with a ChatGPT generated text when @.",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/mastashake08/discord-gpt-bot.git"
  },
  "keywords": [
    "OpenAI",
    "ChatGPT",
    "gpt",
    "discord",
    "bots",
    "discord"
  ],
  "author": "Mastashake08",
  "license": "GPL3",
  "bugs": {
    "url": "https://github.com/mastashake08/discord-gpt-bot/issues"
  },
  "homepage": "https://github.com/mastashake08/discord-gpt-bot#readme",
  "dependencies": {
    "discord.js": "^14.7.1",
    "dotenv": "^16.0.3",
    "openai": "^3.2.1"
  }
}

Index File

This is where most of the magic happens, the index.js file loads our slash commands starts OpenAI, and starts the Discord.js instance. All secret keys and tokens are loaded using the dotenv package from a .env file. The generate function makes a call to the OpenAI.createCompletion() function which returns our text completion.

require('dotenv').config()
const { Client, Events, Collection, REST, Routes } = require('discord.js');
const fs = require('node:fs');
const path = require('node:path');
const { Configuration, OpenAIApi } = require("openai")
const client = new Client({ intents: 2048 })
client.commands = new Collection()
const configuration = new Configuration({
  apiKey: process.env.OPENAI_API_KEY,
});
const openai = new OpenAIApi(configuration);

async function generate(prompt, model="gpt-3.5-turbo") {
  const completion = await openai.createCompletion({
    model: model,
    prompt: prompt
  });
  const text = completion.data.choices[0].text
  return text;
}


// start the discord client and listen for messages
const commandsPath = path.join(__dirname, 'commands');
const commandFiles = fs.readdirSync(commandsPath).filter(file => file.endsWith('.js'));

const commands = []
// Grab the SlashCommandBuilder#toJSON() output of each command's data for deployment
for (const file of commandFiles) {
	const command = require(`./commands/${file}`);
	commands.push(command.data.toJSON());
}

// Construct and prepare an instance of the REST module
const rest = new REST({ version: '10' }).setToken(process.env.DISCORD_TOKEN);

// and deploy your commands!
(async () => {
	try {
		console.log(`Started refreshing ${commands.length} application (/) commands.`);

		// The put method is used to fully refresh all commands in the guild with the current set
		const data = await rest.put(
			Routes.applicationGuildCommands(process.env.DISCORD_CLIENT_ID, process.env.DISCORD_GUILD_ID),
			{ body: commands },
		);

		console.log(`Successfully reloaded ${data.length} application (/) commands.`);
	} catch (error) {
		// And of course, make sure you catch and log any errors!
		console.error(error);
	}
})();

client.login(process.env.DISCORD_TOKEN)
  client.on(Events.InteractionCreate, async interaction => {
    if (interaction.commandName === 'generate-prompt') {
      const res = await generate(interaction.options.getString('prompt'))
      await interaction.reply({ content: res });
    }
    });

Commands

I created a commands directory and inside created a prompt.js file. This file is responsible for using the SlashCommandBuilder class from DIscord.js to create our command and options.

require('dotenv').config()

const { Configuration, OpenAIApi } = require("openai")
const { SlashCommandBuilder } = require('discord.js');
const configuration = new Configuration({
  apiKey: process.env.OPENAI_API_KEY,
});
const openai = new OpenAIApi(configuration);

async function generate(prompt, model="gpt-3.5-turbo") {
  const completion = await openai.createCompletion({
    model: model,
    prompt: prompt
  });
  const text = completion.data.choices[0].text
  return text;
}
module.exports = {
	data: new SlashCommandBuilder()
   .setName('generate-prompt')
   .setDescription('Generate a ChatGPT reponse!')
   .addStringOption(option =>
     option
     .setName('prompt')
     .setDescription('The prompt to generate')
     .setRequired(true)
    ),

	async execute(interaction) {
    const res = await generate(interaction.options.getString('prompt'))
		await interaction.reply(res);
	},
};

Dockerfile

I created a Dockerfile so that anyone can run this application without having to build from source. It creates a node 16 image, copies the code files over, runs npm install , then runs the command. By passing in an --env-file flag to the docker run command will pass in a .env file to the script.

# syntax=docker/dockerfile:1

FROM node:16.15.0
ENV NODE_ENV=production

WORKDIR /

COPY ["package.json", "package-lock.json*", "./"]

RUN npm install --production

COPY . .

CMD [ "node", "index.js" ]

GitHub Actions

Automating the build process is the final part of the project. Whenever I release a new tagged version of the code, the GitHub action packages the Docker Image and publishes it to Docker Hub as well as Github Container Registry. From there I either run the docker image locally on my Raspberry Pi or I will run it in the cloud.

# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.

# GitHub recommends pinning actions to a commit SHA.
# To get a newer version, you will need to update the SHA.
# You can also reference a tag or branch, but the action may change without warning.

name: Publish Docker image to Docker Hub

on:
  release:
    types: [published]

jobs:
  push_to_registry:
    name: Push Docker image to Docker Hub
    runs-on: ubuntu-latest
    steps:
      - name: Check out the repo
        uses: actions/checkout@v3

      - name: Log in to Docker Hub
        uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Extract metadata (tags, labels) for Docker
        id: meta
        uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
        with:
          images: ${{ github.repository }}

      - name: Build and push Docker image
        uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.

# GitHub recommends pinning actions to a commit SHA.
# To get a newer version, you will need to update the SHA.
# You can also reference a tag or branch, but the action may change without warning.

name: Create and publish a Docker image to Github Packages

on:
  release:
    types: [published]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  build-and-push-image:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    steps:
      - name: Checkout repository
        uses: actions/checkout@v3

      - name: Log in to the Container registry
        uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract metadata (tags, labels) for Docker
        id: meta
        uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}

      - name: Build and push Docker image
        uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}

Usage

Via Node

cp .env.example .env

# Set variables
DISCORD_TOKEN=
DISCORD_CHANNEL_ID=
DISCORD_CLIENT_ID=
DISCORD_GUILD_ID=
OPENAI_API_KEY=

# Call program
node index.js

Via Docker

docker run  --env-file=<PATH TO .ENV> -d --name=<NAME> mastashake08/discord-gpt-bot:latest

Follow Me On Social Media

Follow Me On Youtube!

Follow my YouTube account

Get Your Next Domain Cheap & Support The Channel

I use Namecheap for all of my domains! Whenever I need a cheap solution for a proof-of-concept project I grab a domain name for as little as $1! When you sign up and buy your first domain with Namecheap I get a commission, it’s a great way to get a quality service and support this platform!

Get Your Next Domain Cheap
CLICK HERE

Become A Sponsor

Open-source work is free to use but it is not free to develop. If you enjoy my content and would like to see more please consider becoming a sponsor on Github or Patreon! Not only do you support me but you are funding tech programs for at risk youth in Louisville, Kentucky.

Join The Newsletter

By joining the newsletter, you get first access to all of my blogs, events, and other brand-related content delivered directly to your inbox. It’s 100% free and you can opt out at any time!

Check The Shop

You can also consider visiting the official #CodeLife shop! I have my own clothing/accessory line for techies as well as courses designed by me covering a range of software engineering topics.