├── .env.sample ├── .github └── dependabot.yml ├── .gitignore ├── .vscode ├── launch.json └── settings.json ├── LICENSE ├── Procfile ├── README.md ├── bot.py ├── cogs ├── app_commands │ └── __init__.py ├── help │ ├── __init__.py │ └── help_command.py └── ping │ └── __init__.py ├── config.py ├── requirements.txt ├── runtime.txt └── utils ├── __init__.py └── embedder.py /.env.sample: -------------------------------------------------------------------------------- 1 | DISCORD_TOKEN= -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "pip" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # celery beat schedule file 95 | celerybeat-schedule 96 | 97 | # SageMath parsed files 98 | *.sage.py 99 | 100 | # Environments 101 | .env 102 | .venv 103 | env/ 104 | venv/ 105 | ENV/ 106 | env.bak/ 107 | venv.bak/ 108 | 109 | # Spyder project settings 110 | .spyderproject 111 | .spyproject 112 | 113 | # Rope project settings 114 | .ropeproject 115 | 116 | # mkdocs documentation 117 | /site 118 | 119 | # mypy 120 | .mypy_cache/ 121 | .dmypy.json 122 | dmypy.json 123 | 124 | # Pyre type checker 125 | .pyre/ 126 | 127 | # IDE 128 | # .vscode/ -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Python: Module", 9 | "type": "python", 10 | "request": "launch", 11 | "module": "bot" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.formatting.provider": "black", 3 | "[python]": { 4 | "editor.formatOnSave": true 5 | }, 6 | "python.formatting.blackArgs": [ 7 | "--line-length", 8 | "100" 9 | ] 10 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Jonah Lawrence 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | worker: python bot.py -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Discord Bot Template 2 | 3 | [![Discord](https://img.shields.io/discord/819650821314052106?color=7289DA&logo=discord&logoColor=white)](https://discord.gg/fPrdqh3Zfu "Dev Pro Tips Discussion & Support Server") 4 | [![Powered by Nextcord](https://custom-icon-badges.herokuapp.com/badge/-Powered%20by%20Nextcord-0d1620?logo=nextcord)](https://github.com/nextcord/nextcord "Powered by Nextcord Python API Wrapper") 5 | 6 | This repo is a template for easy creation of maintainable Python Discord bots. 7 | 8 | The library used is [Nextcord](https://github.com/nextcord/nextcord), a maintained fork of Discord.py. 9 | 10 | Nextcord documentation: https://nextcord.readthedocs.io/en/latest/ 11 | 12 | 📺 Python Discord Tutorial: https://www.youtube.com/playlist?list=PL9YUC9AZJGFG6larkQJYio_f0V-O1NRjy 13 | 14 | ## How to use 15 | 16 | Click "use this template" at the top of this repo and follow the instructions, or alternatively, initialize a git repo and copy the template files into the directory. 17 | 18 | ```bash 19 | # Create a new folder (replace my-discord-bot with your bot's name) 20 | mkdir my-discord-bot && cd my-discord-bot 21 | # Initialize the folder as a git repository and copy the template files 22 | git init 23 | cp ../discord-bot-template/* . 24 | ``` 25 | 26 | ## Environment variables 27 | 28 | To run your bot, you'll need a token and other secrets set in a `.env` file. 29 | 30 | Create a file called `.env` and place it in the root of your project. 31 | 32 | (You can do this by creating a copy of `.env.sample` and renaming it to `.env`) 33 | 34 | The contents should look something like this (where the part after `=` is the token you received from the Discord Developer Portal) 35 | 36 | ``` 37 | DISCORD_TOKEN=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 38 | ``` 39 | 40 | If you require additional API keys or variables specific to an enviroment, they should also be added here. You can access them by adding a line such as `GUILD_ID = os.getenv("GUILD_ID", "")` to `config.py`. 41 | 42 | ## Heroku 43 | 44 | `runtime.txt` and `Procfile` are used for Heroku configuration and can be deleted in case you do not plan on hosting there. 45 | 46 | Heroku setup tutorial: https://www.youtube.com/watch?v=EreE-0hQibM 47 | 48 | ## IDE Configuration 49 | 50 | IDE config such as the `.vscode` folder do not normally belong on GitHub since they are often specific to a particular environment. To make sure GitHub will ignore the `.vscode` folder uncomment the line at the end of the `.gitignore`. 51 | -------------------------------------------------------------------------------- /bot.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import nextcord 4 | from nextcord.ext import commands 5 | 6 | import config 7 | 8 | 9 | def main(): 10 | # Allows privledged intents for monitoring members joining, roles editing, and role assignments 11 | # These need to be enabled in the developer portal as well 12 | intents = nextcord.Intents.default() 13 | 14 | # Required in order to read messages (eg. prefix commands) 15 | intents.message_content = True 16 | 17 | # To enable the guilds priveleged intent: 18 | # intents.guilds = True 19 | 20 | # To enable the members priveleged intent: 21 | # intents.members = True 22 | 23 | # Set custom status to "Listening to ?help" 24 | activity = nextcord.Activity( 25 | type=nextcord.ActivityType.listening, name=f"{config.BOT_PREFIX}help" 26 | ) 27 | 28 | bot = commands.Bot( 29 | commands.when_mentioned_or(config.BOT_PREFIX), 30 | intents=intents, 31 | activity=activity, 32 | ) 33 | 34 | # Get the modules of all cogs whose directory structure is ./cogs/ 35 | for folder in os.listdir("cogs"): 36 | bot.load_extension(f"cogs.{folder}") 37 | 38 | @bot.listen() 39 | async def on_ready(): 40 | """When Discord is connected""" 41 | assert bot.user is not None 42 | print(f"{bot.user.name} has connected to Discord!") 43 | 44 | # Run Discord bot 45 | bot.run(config.DISCORD_TOKEN) 46 | 47 | 48 | if __name__ == "__main__": 49 | main() 50 | -------------------------------------------------------------------------------- /cogs/app_commands/__init__.py: -------------------------------------------------------------------------------- 1 | import nextcord 2 | from nextcord.ext import commands 3 | from utils import embed_success 4 | 5 | 6 | class ApplicationCommandsExample(commands.Cog, name="App Commands"): 7 | def __init__(self, bot: commands.Bot): 8 | self.bot = bot 9 | 10 | @nextcord.slash_command(description="Slash command that responds with 'Hello World'") 11 | async def my_slash_cmd(self, interaction: nextcord.Interaction): 12 | await interaction.send(embed=embed_success("Hello World!")) 13 | 14 | @nextcord.slash_command(description="Slash command that repeats an argument") 15 | async def my_slash_cmd_2(self, interaction: nextcord.Interaction, my_arg: str): 16 | await interaction.send(embed=embed_success(f"You said: {my_arg}")) 17 | 18 | @nextcord.user_command() 19 | async def my_user_cmd(self, interaction: nextcord.Interaction, member: nextcord.Member): 20 | await interaction.send(f"Hello, {member.mention}!") 21 | 22 | @nextcord.message_command() 23 | async def my_message_cmd(self, interaction: nextcord.Interaction, message: nextcord.Message): 24 | await interaction.send(f"Hello, {message.author.mention}!") 25 | 26 | 27 | # This function will be called when this extension is loaded. 28 | # It is necessary to add these functions to the bot. 29 | def setup(bot: commands.Bot): 30 | bot.add_cog(ApplicationCommandsExample(bot)) 31 | -------------------------------------------------------------------------------- /cogs/help/__init__.py: -------------------------------------------------------------------------------- 1 | from nextcord.ext import commands 2 | 3 | from .help_command import NewHelpCommand 4 | 5 | 6 | class HelpCog(commands.Cog): 7 | def __init__(self, bot: commands.Bot): 8 | self.bot = bot 9 | self._original_help_command = bot.help_command 10 | bot.help_command = NewHelpCommand() 11 | bot.help_command.cog = self 12 | 13 | def cog_unload(self): 14 | self.bot.help_command = self._original_help_command 15 | 16 | 17 | # setup functions for bot 18 | def setup(bot: commands.Bot): 19 | bot.add_cog(HelpCog(bot)) 20 | -------------------------------------------------------------------------------- /cogs/help/help_command.py: -------------------------------------------------------------------------------- 1 | import config 2 | import nextcord 3 | from nextcord.embeds import EmptyEmbed 4 | from nextcord.ext import commands 5 | 6 | 7 | class NewHelpCommand(commands.MinimalHelpCommand): 8 | """Custom help command override using embeds""" 9 | 10 | COLOUR = nextcord.Colour.blurple() 11 | 12 | def get_ending_note(self): 13 | """Returns note to display at the bottom""" 14 | invoked_with = self.invoked_with 15 | return f"Use {config.BOT_PREFIX}{invoked_with} [command] for more info on a command." 16 | 17 | def get_command_signature(self, command: commands.core.Command): 18 | """Retrieves the signature portion of the help page.""" 19 | return f"{command.qualified_name} {command.signature}" 20 | 21 | async def send_bot_help(self, mapping: dict): 22 | """implements bot command help page""" 23 | embed = nextcord.Embed(title="Bot Commands", colour=self.COLOUR) 24 | avatar = self.context.bot.user.avatar 25 | avatar_url = avatar.url if avatar else EmptyEmbed 26 | embed.set_author(name=self.context.bot.user.name, icon_url=avatar_url) 27 | description = self.context.bot.description 28 | if description: 29 | embed.description = description 30 | 31 | for cog, commands in mapping.items(): 32 | name = "No Category" if cog is None else cog.qualified_name 33 | filtered = await self.filter_commands(commands, sort=True) 34 | if filtered: 35 | # \u2002 = middle dot 36 | value = "\u2002".join(f"{config.BOT_PREFIX}{c.name}" for c in filtered) 37 | if cog and cog.description: 38 | value = f"{cog.description}\n{value}" 39 | embed.add_field(name=name, value=value) 40 | 41 | embed.set_footer(text=self.get_ending_note()) 42 | await self.get_destination().send(embed=embed) 43 | 44 | async def send_cog_help(self, cog: commands.Cog): 45 | """implements cog help page""" 46 | embed = nextcord.Embed(title=f"{cog.qualified_name} Commands", colour=self.COLOUR) 47 | if cog.description: 48 | embed.description = cog.description 49 | 50 | filtered = await self.filter_commands(cog.get_commands(), sort=True) 51 | for command in filtered: 52 | embed.add_field( 53 | name=self.get_command_signature(command), 54 | value=command.short_doc or "...", 55 | inline=False, 56 | ) 57 | 58 | embed.set_footer(text=self.get_ending_note()) 59 | await self.get_destination().send(embed=embed) 60 | 61 | async def send_group_help(self, group: commands.Group): 62 | """implements group help page and command help page""" 63 | embed = nextcord.Embed(title=group.qualified_name, colour=self.COLOUR) 64 | if group.help: 65 | embed.description = group.help 66 | 67 | if isinstance(group, commands.Group): 68 | filtered = await self.filter_commands(group.commands, sort=True) 69 | for command in filtered: 70 | embed.add_field( 71 | name=self.get_command_signature(command), 72 | value=command.short_doc or "...", 73 | inline=False, 74 | ) 75 | 76 | embed.set_footer(text=self.get_ending_note()) 77 | await self.get_destination().send(embed=embed) 78 | 79 | # Use the same function as group help for command help 80 | send_command_help = send_group_help # type: ignore 81 | -------------------------------------------------------------------------------- /cogs/ping/__init__.py: -------------------------------------------------------------------------------- 1 | from nextcord.ext import commands 2 | from utils import embed_success 3 | 4 | 5 | class Ping(commands.Cog, name="Ping"): 6 | def __init__(self, bot: commands.Bot): 7 | self.bot = bot 8 | 9 | @commands.command(name="ping") 10 | async def ping(self, ctx: commands.Context): 11 | """A command which simply acknowledges the user's ping. 12 | Usage: 13 | ``` 14 | ?ping 15 | ``` 16 | """ 17 | # log in console that a ping was received 18 | print("Received ping") 19 | # respond to the message 20 | await ctx.send(embed=embed_success("Pong!")) 21 | 22 | 23 | # This function will be called when this extension is loaded. 24 | # It is necessary to add these functions to the bot. 25 | def setup(bot: commands.Bot): 26 | bot.add_cog(Ping(bot)) 27 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from dotenv.main import load_dotenv 4 | 5 | load_dotenv() 6 | 7 | # Discord config 8 | DISCORD_TOKEN = os.getenv("DISCORD_TOKEN", "") 9 | BOT_PREFIX = "?" 10 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | nextcord==2.6.0 2 | python-dotenv==1.0.0 3 | -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.10.4 2 | -------------------------------------------------------------------------------- /utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .embedder import * 2 | -------------------------------------------------------------------------------- /utils/embedder.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | import nextcord 4 | 5 | DEFAULT_COLOR = nextcord.Colour.blurple() 6 | MAX_EMBED_DESCRIPTION_LENGTH = 4096 7 | MAX_EMBED_FIELD_TITLE_LENGTH = 256 8 | MAX_EMBED_FIELD_FOOTER_LENGTH = 2048 9 | 10 | 11 | def __trim(text: str, limit: int) -> str: 12 | """limit text to a certain number of characters""" 13 | return text[: limit - 3].strip() + "..." if len(text) > limit else text 14 | 15 | 16 | def embed_success( 17 | title: str, 18 | description: Optional[str] = None, 19 | footer: Optional[str] = None, 20 | url: Optional[str] = None, 21 | image: Optional[str] = None, 22 | thumbnail: Optional[str] = None, 23 | ) -> nextcord.Embed: 24 | """Embed a success message and an optional description, footer, and url""" 25 | return build_embed(title, description, footer, url, nextcord.Colour.green(), image, thumbnail) 26 | 27 | 28 | def embed_warning( 29 | title: str, 30 | description: Optional[str] = None, 31 | footer: Optional[str] = None, 32 | url: Optional[str] = None, 33 | image: Optional[str] = None, 34 | thumbnail: Optional[str] = None, 35 | ) -> nextcord.Embed: 36 | """Embed a warning message and an optional description, footer, and url""" 37 | return build_embed(title, description, footer, url, nextcord.Colour.gold(), image, thumbnail) 38 | 39 | 40 | def embed_error( 41 | title: str, 42 | description: Optional[str] = None, 43 | footer: Optional[str] = None, 44 | url: Optional[str] = None, 45 | image: Optional[str] = None, 46 | thumbnail: Optional[str] = None, 47 | ) -> nextcord.Embed: 48 | """Embed an error message and an optional description, footer, and url""" 49 | return build_embed(title, description, footer, url, nextcord.Colour.red(), image, thumbnail) 50 | 51 | 52 | def build_embed( 53 | title: str, 54 | description: Optional[str] = None, 55 | footer: Optional[str] = None, 56 | url: Optional[str] = None, 57 | colour: nextcord.Colour = DEFAULT_COLOR, 58 | image: Optional[str] = None, 59 | thumbnail: Optional[str] = None, 60 | ) -> nextcord.Embed: 61 | """Embed a message and an optional description, footer, and url""" 62 | # create the embed 63 | embed = nextcord.Embed( 64 | title=__trim(title, MAX_EMBED_FIELD_TITLE_LENGTH), url=url, colour=colour 65 | ) 66 | if description: 67 | embed.description = __trim(description, MAX_EMBED_DESCRIPTION_LENGTH) 68 | if footer: 69 | embed.set_footer(text=__trim(footer, MAX_EMBED_FIELD_FOOTER_LENGTH)) 70 | if image: 71 | embed.set_image(url=image) 72 | if thumbnail: 73 | embed.set_thumbnail(url=thumbnail) 74 | return embed 75 | --------------------------------------------------------------------------------