├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ └── fly.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── Procfile ├── README.md ├── bot.py ├── fly.toml ├── modules ├── help │ ├── __init__.py │ ├── cog.py │ └── help_command.py ├── ping │ ├── __init__.py │ └── cog.py └── random │ ├── __init__.py │ └── cog.py ├── requirements.txt └── runtime.txt /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.github/workflows/fly.yml: -------------------------------------------------------------------------------- 1 | name: Fly Deploy 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | env: 9 | FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} 10 | 11 | jobs: 12 | deploy: 13 | name: Deploy app 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | 18 | - uses: superfly/flyctl-actions/setup-flyctl@master 19 | 20 | - run: flyctl deploy --remote-only 21 | -------------------------------------------------------------------------------- /.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 | # VS code stuff 128 | .vscode/ 129 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10 2 | WORKDIR /bot 3 | COPY requirements.txt /bot/ 4 | RUN pip install -r requirements.txt 5 | COPY . /bot 6 | CMD python bot.py -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 DenverCoder1 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 | # Tutorial Discord Bot 2 | 3 |

4 | 5 | 6 |

7 | 8 | This is the source code for the Bot features I will be showing you how to make on YouTube. 9 | 10 | Check out the playlist [here](https://www.youtube.com/playlist?list=PL9YUC9AZJGFG6larkQJYio_f0V-O1NRjy). 11 | 12 | Don't forget to [subscribe](https://www.youtube.com/c/DevProTips?sub_confirmation=1)! 13 | 14 | Also check out my [Discord bot template](https://github.com/DenverCoder1/discord-bot-template) for quick setup. 15 | -------------------------------------------------------------------------------- /bot.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dotenv import load_dotenv 3 | from nextcord.ext import commands 4 | 5 | def main(): 6 | client = commands.Bot(command_prefix="?") 7 | 8 | load_dotenv() 9 | 10 | @client.event 11 | async def on_ready(): 12 | print(f"{client.user.name} has connected to Discord.") 13 | 14 | # load all cogs 15 | for folder in os.listdir("modules"): 16 | if os.path.exists(os.path.join("modules", folder, "cog.py")): 17 | client.load_extension(f"modules.{folder}.cog") 18 | 19 | client.run(os.getenv("DISCORD_TOKEN")) 20 | 21 | if __name__ == '__main__': 22 | main() -------------------------------------------------------------------------------- /fly.toml: -------------------------------------------------------------------------------- 1 | # fly.toml file generated for tutorial-bot on 2022-08-28T09:30:27-06:00 2 | 3 | app = "tutorial-bot" 4 | kill_signal = "SIGINT" 5 | kill_timeout = 5 6 | processes = [] 7 | 8 | [env] 9 | -------------------------------------------------------------------------------- /modules/help/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenverCoder1/tutorial-discord-bot/8133f1c38fa290198125e48b044c5cd934246fb0/modules/help/__init__.py -------------------------------------------------------------------------------- /modules/help/cog.py: -------------------------------------------------------------------------------- 1 | from nextcord.ext import commands 2 | 3 | from .help_command import MyHelpCommand 4 | 5 | class HelpCog(commands.Cog, name="Help"): 6 | """Shows help info for commands and cogs""" 7 | 8 | COG_EMOJI = "❔" 9 | 10 | def __init__(self, bot): 11 | self._original_help_command = bot.help_command 12 | bot.help_command = MyHelpCommand() 13 | bot.help_command.cog = self 14 | 15 | def cog_unload(self): 16 | self.bot.help_command = self._original_help_command 17 | 18 | def setup(bot: commands.Bot): 19 | bot.add_cog(HelpCog(bot)) 20 | -------------------------------------------------------------------------------- /modules/help/help_command.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Set 2 | from nextcord.ext import commands 3 | from nextcord import Embed 4 | import nextcord 5 | 6 | class HelpDropdown(nextcord.ui.Select): 7 | def __init__(self, help_command: "MyHelpCommand", options: list[nextcord.SelectOption]): 8 | super().__init__(placeholder="Choose a category...", min_values=1, max_values=1, options=options) 9 | self._help_command = help_command 10 | 11 | async def callback(self, interaction: nextcord.Interaction): 12 | embed = ( 13 | await self._help_command.cog_help_embed(self._help_command.context.bot.get_cog(self.values[0])) 14 | if self.values[0] != self.options[0].value 15 | else await self._help_command.bot_help_embed(self._help_command.get_bot_mapping()) 16 | ) 17 | await interaction.response.edit_message(embed=embed) 18 | 19 | 20 | class HelpView(nextcord.ui.View): 21 | def __init__(self, help_command: "MyHelpCommand", options: list[nextcord.SelectOption], *, timeout: Optional[float] = 120.0): 22 | super().__init__(timeout=timeout) 23 | self.add_item(HelpDropdown(help_command, options)) 24 | self._help_command = help_command 25 | 26 | async def on_timeout(self): 27 | # remove dropdown from message on timeout 28 | self.clear_items() 29 | await self._help_command.response.edit(view=self) 30 | 31 | async def interaction_check(self, interaction: nextcord.Interaction) -> bool: 32 | return self._help_command.context.author == interaction.user 33 | 34 | 35 | class MyHelpCommand(commands.MinimalHelpCommand): 36 | def get_command_signature(self, command): 37 | return f"{self.context.clean_prefix}{command.qualified_name} {command.signature}" 38 | 39 | async def _cog_select_options(self) -> list[nextcord.SelectOption]: 40 | options: list[nextcord.SelectOption] = [] 41 | options.append(nextcord.SelectOption( 42 | label="Home", 43 | emoji="🏠", 44 | description="Go back to the main menu.", 45 | )) 46 | 47 | for cog, command_set in self.get_bot_mapping().items(): 48 | filtered = await self.filter_commands(command_set, sort=True) 49 | if not filtered: 50 | continue 51 | emoji = getattr(cog, "COG_EMOJI", None) 52 | options.append(nextcord.SelectOption( 53 | label=cog.qualified_name if cog else "No Category", 54 | emoji=emoji, 55 | description=cog.description[:100] if cog and cog.description else None 56 | )) 57 | 58 | return options 59 | 60 | async def _help_embed( 61 | self, title: str, description: Optional[str] = None, mapping: Optional[str] = None, 62 | command_set: Optional[Set[commands.Command]] = None, set_author: bool = False 63 | ) -> Embed: 64 | embed = Embed(title=title) 65 | if description: 66 | embed.description = description 67 | if set_author: 68 | avatar = self.context.bot.user.avatar or self.context.bot.user.default_avatar 69 | embed.set_author(name=self.context.bot.user.name, icon_url=avatar.url) 70 | if command_set: 71 | # show help about all commands in the set 72 | filtered = await self.filter_commands(command_set, sort=True) 73 | for command in filtered: 74 | embed.add_field( 75 | name=self.get_command_signature(command), 76 | value=command.short_doc or "...", 77 | inline=False 78 | ) 79 | elif mapping: 80 | # add a short description of commands in each cog 81 | for cog, command_set in mapping.items(): 82 | filtered = await self.filter_commands(command_set, sort=True) 83 | if not filtered: 84 | continue 85 | name = cog.qualified_name if cog else "No category" 86 | emoji = getattr(cog, "COG_EMOJI", None) 87 | cog_label = f"{emoji} {name}" if emoji else name 88 | # \u2002 is an en-space 89 | cmd_list = "\u2002".join( 90 | f"`{self.context.clean_prefix}{cmd.name}`" for cmd in filtered 91 | ) 92 | value = ( 93 | f"{cog.description}\n{cmd_list}" 94 | if cog and cog.description 95 | else cmd_list 96 | ) 97 | embed.add_field(name=cog_label, value=value) 98 | return embed 99 | 100 | async def bot_help_embed(self, mapping: dict) -> Embed: 101 | return await self._help_embed( 102 | title="Bot Commands", 103 | description=self.context.bot.description, 104 | mapping=mapping, 105 | set_author=True, 106 | ) 107 | 108 | async def send_bot_help(self, mapping: dict): 109 | embed = await self.bot_help_embed(mapping) 110 | options = await self._cog_select_options() 111 | self.response = await self.get_destination().send(embed=embed, view=HelpView(self, options)) 112 | 113 | async def send_command_help(self, command: commands.Command): 114 | emoji = getattr(command.cog, "COG_EMOJI", None) 115 | embed = await self._help_embed( 116 | title=f"{emoji} {command.qualified_name}" if emoji else command.qualified_name, 117 | description=command.help, 118 | command_set=command.commands if isinstance(command, commands.Group) else None 119 | ) 120 | await self.get_destination().send(embed=embed) 121 | 122 | async def cog_help_embed(self, cog: Optional[commands.Cog]) -> Embed: 123 | if cog is None: 124 | return await self._help_embed( 125 | title=f"No category", 126 | command_set=self.get_bot_mapping()[None] 127 | ) 128 | emoji = getattr(cog, "COG_EMOJI", None) 129 | return await self._help_embed( 130 | title=f"{emoji} {cog.qualified_name}" if emoji else cog.qualified_name, 131 | description=cog.description, 132 | command_set=cog.get_commands() 133 | ) 134 | 135 | async def send_cog_help(self, cog: commands.Cog): 136 | embed = await self.cog_help_embed(cog) 137 | await self.get_destination().send(embed=embed) 138 | 139 | # Use the same function as command help for group help 140 | send_group_help = send_command_help 141 | -------------------------------------------------------------------------------- /modules/ping/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenverCoder1/tutorial-discord-bot/8133f1c38fa290198125e48b044c5cd934246fb0/modules/ping/__init__.py -------------------------------------------------------------------------------- /modules/ping/cog.py: -------------------------------------------------------------------------------- 1 | from nextcord.ext import commands 2 | 3 | class Ping(commands.Cog, name="Ping"): 4 | """Receives ping commands""" 5 | 6 | COG_EMOJI = "🏓" 7 | 8 | def __init__(self, bot: commands.Bot): 9 | self._bot = bot 10 | 11 | @commands.command() 12 | async def ping(self, ctx: commands.Context): 13 | """Checks for a response from the bot""" 14 | await ctx.send("Pong") 15 | 16 | def setup(bot: commands.Bot): 17 | bot.add_cog(Ping(bot)) -------------------------------------------------------------------------------- /modules/random/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenverCoder1/tutorial-discord-bot/8133f1c38fa290198125e48b044c5cd934246fb0/modules/random/__init__.py -------------------------------------------------------------------------------- /modules/random/cog.py: -------------------------------------------------------------------------------- 1 | from nextcord.ext import commands 2 | import random 3 | 4 | class Random(commands.Cog, name="Random"): 5 | """Returns random results""" 6 | 7 | COG_EMOJI = "🎲" 8 | 9 | def __init__(self, bot: commands.Bot): 10 | self._bot = bot 11 | 12 | @commands.command() 13 | async def roll(self, ctx: commands.Context, dice: str): 14 | """Rolls a given amount of dice in the form \_d\_ 15 | 16 | Example: 17 | ``` 18 | ?roll 2d20 19 | ``` 20 | """ 21 | try: 22 | rolls = "" 23 | total = 0 24 | amount, die = dice.split("d") 25 | for _ in range(int(amount)): 26 | roll = random.randint(1, int(die)) 27 | total += roll 28 | rolls += f"{roll} " 29 | await ctx.send(f"Rolls: {rolls}\nSum: {total}") 30 | except ValueError: 31 | await ctx.send("Dice must be in the format \_d\_ (example: 2d6)") 32 | 33 | @commands.command() 34 | async def choose(self, ctx: commands.Context, *args): 35 | """Chooses a random item from a list 36 | 37 | Example: 38 | ``` 39 | ?choose "First Option" "Second Option" "Third Option" 40 | ``` 41 | """ 42 | try: 43 | choice = random.choice(args) 44 | await ctx.send(choice) 45 | except IndexError: 46 | await ctx.send("You must specify at least one argument.") 47 | 48 | def setup(bot: commands.Bot): 49 | bot.add_cog(Random(bot)) -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | nextcord==2.6.0 2 | python-dotenv==1.0.0 -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.9.1 --------------------------------------------------------------------------------