├── .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
--------------------------------------------------------------------------------