Skip to content

Commit

Permalink
feat(discord): add dynamic slash commands (#277)
Browse files Browse the repository at this point in the history
  • Loading branch information
ReenigneArcher committed Apr 30, 2024
1 parent a19e9a6 commit ad71781
Show file tree
Hide file tree
Showing 7 changed files with 181 additions and 40 deletions.
3 changes: 3 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# ensure dockerfiles are checked out with LF line endings
Dockerfile text eol=lf
*.dockerfile text eol=lf
14 changes: 13 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1.4
# artifacts: false
# platforms: linux/amd64
FROM python:3.11.3-slim-bullseye
FROM python:3.11-slim-bookworm

# Basic config
ARG DAILY_TASKS=true
Expand Down Expand Up @@ -37,6 +37,18 @@ ENV DISCORD_WEBHOOK=$DISCORD_WEBHOOK
ENV GRAVATAR_EMAIL=$GRAVATAR_EMAIL
ENV REDIRECT_URI=$REDIRECT_URI

SHELL ["/bin/bash", "-o", "pipefail", "-c"]
# install dependencies
RUN <<_DEPS
#!/bin/bash
set -e
apt-get update -y
apt-get install -y --no-install-recommends \
git
apt-get clean
rm -rf /var/lib/apt/lists/*
_DEPS

VOLUME /data

WORKDIR /app/
Expand Down
47 changes: 19 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,9 @@ platforms such as GitHub discussions/issues could be added.

### Discord Slash Commands

| command | description | argument 1 |
|----------|---------------------------------------------------|---------------------|
| /help | Return help message | |
| /channel | Suggest to move discussion to a different channel | recommended_channel |
| /docs | Return the specified docs page | user |
| /donate | Return donation links | user |
| /random | Return a random video game quote | |
| command | description |
|----------|----------------------------------------------------------|
| /help | Return help message, for a list of all possible commands |


## Instructions
Expand All @@ -32,16 +28,18 @@ platforms such as GitHub discussions/issues could be added.
:exclamation: if using Docker these can be arguments.
:warning: Never publicly expose your tokens, secrets, or ids.

| variable | required | default | description |
|----------------------|----------|---------|---------------------------------------------------------------|
| DISCORD_BOT_TOKEN | True | None | Token from Bot page on discord developer portal. |
| DAILY_TASKS | False | true | Daily tasks on or off. |
| DAILY_RELEASES | False | true | Send a message for each game released on this day in history. |
| DAILY_CHANNEL_ID | False | None | Required if daily_tasks is enabled. |
| DAILY_TASKS_UTC_HOUR | False | 12 | The hour to run daily tasks. |
| GRAVATAR_EMAIL | False | None | Gravatar email address for bot avatar. |
| IGDB_CLIENT_ID | False | None | Required if daily_releases is enabled. |
| IGDB_CLIENT_SECRET | False | None | Required if daily_releases is enabled. |
| variable | required | default | description |
|-------------------------|----------|------------------------------------------------------|---------------------------------------------------------------|
| DISCORD_BOT_TOKEN | True | `None` | Token from Bot page on discord developer portal. |
| DAILY_TASKS | False | `true` | Daily tasks on or off. |
| DAILY_RELEASES | False | `true` | Send a message for each game released on this day in history. |
| DAILY_CHANNEL_ID | False | `None` | Required if daily_tasks is enabled. |
| DAILY_TASKS_UTC_HOUR | False | `12` | The hour to run daily tasks. |
| GRAVATAR_EMAIL | False | `None` | Gravatar email address for bot avatar. |
| IGDB_CLIENT_ID | False | `None` | Required if daily_releases is enabled. |
| IGDB_CLIENT_SECRET | False | `None` | Required if daily_releases is enabled. |
| SUPPORT_COMMANDS_REPO | False | `https://github.com/LizardByte/support-bot-commands` | Repository for support commands. |
| SUPPORT_COMMANDS_BRANCH | False | `master` | Branch for support commands. |

* Running bot:
* `python -m src`
Expand All @@ -52,9 +50,7 @@ platforms such as GitHub discussions/issues could be added.
### Reddit

* Set up an application at [reddit apps](https://www.reddit.com/prefs/apps/).
* The redirect uri must be publicly accessible.
* If using Replit, enter `https://<REPL_SLUG>.<REPL_OWNER>.repl.co`
* Otherwise, it is recommended to use [Nginx Proxy Manager](https://nginxproxymanager.com/) and [Duck DNS](https://www.duckdns.org/)
* The redirect uri should be https://localhost:8080
* Take note of the `client_id` and `client_secret`
* Enter the following as environment variables

Expand All @@ -65,13 +61,8 @@ platforms such as GitHub discussions/issues could be added.
| PRAW_SUBREDDIT | True | None | Subreddit to monitor (reddit user should be moderator of the subreddit) |
| DISCORD_WEBHOOK | False | None | URL of webhook to send discord notifications to |
| GRAVATAR_EMAIL | False | None | Gravatar email address to get avatar from |
| REDIRECT_URI | True | None | The redirect URI entered during the reddit application setup |
| REDDIT_USERNAME | True | None | Reddit username |
* | REDDIT_PASSWORD | True | None | Reddit password |

* First run (or manually get a new refresh token):
* Delete `./data/refresh_token` file if needed
* `python -m src`
* Open browser and login to reddit account to use with bot
* Navigate to URL printed in console and accept
* `./data/refresh_token` file is written
* Running after refresh_token already obtained:
* Running bot:
* `python -m src`
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
beautifulsoup4==4.12.3
Flask==3.0.3
GitPython==3.1.43
igdb-api-v4==0.3.2
libgravatar==1.0.4
mistletoe==1.3.0
praw==7.7.1
py-cord==2.5.0
python-dotenv==1.0.1
Expand Down
12 changes: 12 additions & 0 deletions src/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,20 @@ def get_avatar_bytes():
return avatar_img


def get_data_dir():
# parent directory name of this file, not full path
parent_dir = os.path.dirname(os.path.abspath(__file__)).split(os.sep)[-1]
if parent_dir == 'app': # running in Docker container
d = '/data'
else: # running locally
d = os.path.join(os.getcwd(), 'data')
os.makedirs(d, exist_ok=True)
return d


# constants
avatar = get_bot_avatar(gravatar=os.environ['GRAVATAR_EMAIL'])
org_name = 'LizardByte'
bot_name = f'{org_name}-Bot'
bot_url = 'https://app.lizardbyte.dev'
data_dir = get_data_dir()
133 changes: 131 additions & 2 deletions src/discord/cogs/support_commands.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,145 @@
# standard imports
import datetime
import os

# lib imports
import discord
from discord.commands import Option
from discord.ext import tasks
import git
import mistletoe
from mistletoe.markdown_renderer import MarkdownRenderer

# local imports
from src.common import avatar, bot_name
from src.common import avatar, bot_name, data_dir
from src.discord.views import DocsCommandView
from src.discord import cogs_common


class SupportCommandsCog(discord.Cog):
def __init__(self, bot):
self.bot = bot
self.bot: discord.Bot = bot

self.commands = {}
self.commands_for_removal = []

self.repo_url = os.getenv("SUPPORT_COMMANDS_REPO", "https://github.com/LizardByte/support-bot-commands")
self.repo_branch = os.getenv("SUPPORT_COMMANDS_BRANCH", "master")
self.local_dir = os.path.join(data_dir, "support-bot-commands")
self.commands_dir = os.path.join(self.local_dir, "docs")
self.relative_commands_dir = os.path.relpath(self.commands_dir, self.local_dir)

@discord.Cog.listener()
async def on_ready(self):
# Clone/update the repository
self.update_repo()

# Create commands
self.create_commands()

# Start the self update task
self.self_update.start()

@tasks.loop(minutes=15.0)
async def self_update(self):
self.update_repo()
self.create_commands()
await self.bot.sync_commands()

def update_repo(self):
# Clone or pull the repository
if not os.path.exists(self.local_dir):
repo = git.Repo.clone_from(self.repo_url, self.local_dir)
else:
repo = git.Repo(self.local_dir)
origin = repo.remotes.origin

# Fetch the latest changes from the upstream
origin.fetch()

# Reset the local branch to match the upstream
repo.git.reset('--hard', f'origin/{self.repo_branch}')

for f in repo.untracked_files:
# remove untracked files
os.remove(os.path.join(self.local_dir, f))

# Checkout the branch
repo.git.checkout(self.repo_branch)

def get_project_commands(self):
projects = []
for project in os.listdir(self.commands_dir):
project_dir = os.path.join(self.commands_dir, project)
if os.path.isdir(project_dir):
projects.append(project)
return projects

def create_commands(self):
for project in self.get_project_commands():
project_dir = os.path.join(self.commands_dir, project)
if os.path.isdir(project_dir):
self.create_project_commands(project=project, project_dir=project_dir)

def create_project_commands(self, project, project_dir):
# Get the list of commands in the project directory
command_choices = []
for cmd in os.listdir(project_dir):
cmd_path = os.path.join(project_dir, cmd)
if os.path.isfile(cmd_path) and cmd.endswith('.md'):
cmd_name = os.path.splitext(cmd)[0]
command_choices.append(discord.OptionChoice(name=cmd_name, value=cmd_name))

# Check if a command with the same name already exists
if project in self.commands:
# Update the command options
project_command = self.commands[project]
project_command.options = [
Option(
name='command',
description='The command to run',
type=discord.SlashCommandOptionType.string,
choices=command_choices,
required=True,
)
]
else:
# Create a slash command for the project
@self.bot.slash_command(name=project, description=f"Commands for the {project} project.",
options=[
Option(
name='command',
description='The command to run',
type=discord.SlashCommandOptionType.string,
choices=command_choices,
required=True,
)
])
async def project_command(ctx: discord.ApplicationContext, command: str):
# Determine the command file path
command_file = os.path.join(project_dir, f"{command}.md")

# Read the command file
with open(command_file, "r", encoding='utf-8') as file:
with MarkdownRenderer(
max_line_length=4096, # this must be set to reflow the text
normalize_whitespace=True) as renderer:
description = renderer.render(mistletoe.Document(file))

source_url = (f"{self.repo_url}/blob/{self.repo_branch}/{self.relative_commands_dir}/"
f"{project}/{command}.md")

embed = discord.Embed(
color=0xF1C232,
description=description,
timestamp=datetime.datetime.now(tz=datetime.timezone.utc),
title="See on GitHub",
url=source_url,
)
embed.set_footer(text=f"Requested by {ctx.author.display_name}")
await ctx.respond(embed=embed, ephemeral=False)

self.commands[project] = project_command

@discord.slash_command(
name="docs",
Expand Down
10 changes: 1 addition & 9 deletions src/reddit/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,7 @@ def __init__(self, **kwargs):
self.redirect_uri = kwargs['redirect_uri']

# directories
# parent directory name of this file, not full path
parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))).split(os.sep)[-1]
print(f'PARENT_DIR: {parent_dir}')
if parent_dir == 'app': # running in Docker container
self.data_dir = '/data'
else: # running locally
self.data_dir = os.path.join(os.getcwd(), 'data')
print(F'DATA_DIR: {self.data_dir}')
os.makedirs(self.data_dir, exist_ok=True)
self.data_dir = common.data_dir

self.last_online_file = os.path.join(self.data_dir, 'last_online')
self.reddit = praw.Reddit(
Expand Down

0 comments on commit ad71781

Please sign in to comment.