├── requirements.txt ├── .env.example ├── .dockerignore ├── Dockerfile ├── database ├── schema.sql └── __init__.py ├── docker-compose.yml ├── .github └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── cogs ├── template.py ├── fun.py ├── owner.py ├── general.py └── moderation.py ├── CONTRIBUTING.md ├── .gitignore ├── CODE_OF_CONDUCT.md ├── README.md ├── UPDATES.md ├── LICENSE.md └── bot.py /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp 2 | aiosqlite 3 | discord.py==2.6.3 4 | python-dotenv -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | TOKEN=YOUR_BOT_TOKEN_HERE 2 | PREFIX=YOUR_BOT_PREFIX_HERE 3 | INVITE_LINK=YOUR_BOT_INVITE_LINK_HERE -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git/ 2 | .github/ 3 | database/ 4 | venv/ 5 | .env* 6 | CODE_OF_CONDUCT.md 7 | CONTRIBUTING.md 8 | UPDATES.md -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.12.9-slim-bookworm 2 | 3 | WORKDIR /bot 4 | COPY . /bot 5 | 6 | RUN python -m pip install -r requirements.txt 7 | 8 | ENTRYPOINT [ "python", "bot.py" ] -------------------------------------------------------------------------------- /database/schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS `warns` ( 2 | `id` int(11) NOT NULL, 3 | `user_id` varchar(20) NOT NULL, 4 | `server_id` varchar(20) NOT NULL, 5 | `moderator_id` varchar(20) NOT NULL, 6 | `reason` varchar(255) NOT NULL, 7 | `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP 8 | ); -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | discord-bot: 3 | build: . 4 | image: python-discord-bot-template 5 | env_file: 6 | - .env 7 | volumes: 8 | - ./database:/bot/database 9 | - ./discord.log:/bot/discord.log 10 | 11 | # Alternatively you can set the environment variables as such: 12 | # /!\ The token shouldn't be written here, as this file is not ignored from Git /!\ 13 | # environment: 14 | # - PREFIX=YOUR_BOT_PREFIX_HERE 15 | # - INVITE_LINK=YOUR_BOT_INVITE_LINK_HERE -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for the template 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help improve the template 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | 1. Given the following code ... 16 | 2. Using the following command '...' 17 | 3. Given the following arguments '...' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Template Version** 27 | The version of the template that you are using. 28 | 29 | **Additional context** 30 | Add any other context about the problem here. 31 | -------------------------------------------------------------------------------- /cogs/template.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright © Krypton 2019-Present - https://github.com/kkrypt0nn (https://krypton.ninja) 3 | Description: 4 | 🐍 A simple template to start to code your own and personalized Discord bot in Python 5 | 6 | Version: 6.4.0 7 | """ 8 | 9 | from discord.ext import commands 10 | from discord.ext.commands import Context 11 | 12 | 13 | # Here we name the cog and create a new class for the cog. 14 | class Template(commands.Cog, name="template"): 15 | def __init__(self, bot) -> None: 16 | self.bot = bot 17 | 18 | # Here you can just add your own commands, you'll always need to provide "self" as first parameter. 19 | 20 | @commands.hybrid_command( 21 | name="testcommand", 22 | description="This is a testing command that does nothing.", 23 | ) 24 | async def testcommand(self, context: Context) -> None: 25 | """ 26 | This is a testing command that does nothing. 27 | 28 | :param context: The application command context. 29 | """ 30 | # Do your stuff here 31 | 32 | # Don't forget to remove "pass", I added this just because there's no content in the method. 33 | pass 34 | 35 | 36 | # And then we finally add the cog to the bot so that it can load, unload, reload and use it's content. 37 | async def setup(bot) -> None: 38 | await bot.add_cog(Template(bot)) 39 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Your input is amazing! Making contributing to this project as easy and transparent as possible is one of the most important side, this includes: 4 | 5 | - Reporting a bug 6 | - Discussing the current state of the code 7 | - Submitting a fix 8 | - Proposing new features 9 | - Becoming a maintainer 10 | 11 | ## Wanted changes 12 | 13 | - New features 14 | - Better documentation 15 | - Fixing of spelling and grammatical issues 16 | 17 | ## Unwanted changes 18 | 19 | - Whitespaces and punctuation changes 20 | - Word changes using synonyms 21 | - Entire rewrites of the project, or parts of the project - unless approved first by a maintainer 22 | 23 | ## All code changes happen through pull requests 24 | 25 | Pull requests are the best way to propose changes to the codebase. We actively welcome your pull requests: 26 | 27 | 1. Fork the repo and create your branch from `main`. 28 | 2. Keep consistency with the current state of the codebase, this includes but is not limited to naming convention, Discord embeds, etc. 29 | 3. Format the code of the **Python** files you've edited with the **black** formatter and **all others** with the **Prettier** formatter. 30 | 4. Sort the imports with `isort` 31 | 5. Issue that pull request! 32 | 33 | ## Commit messages guidelines 34 | 35 | This project uses [`Conventional Commits 1.0.0`](https://conventionalcommits.org/en/v1.0.0/) hence your commit messages **must** follow the same convention or your contributions will be ignored, refused or assigned to another user or maintainer. 36 | 37 | ## Create a GitHub [Issue](https://github.com/kkrypt0nn/Python-Discord-Bot-Template/issues) and **then** a pull request 38 | 39 | Start contributing by first [opening a new issue](https://github.com/kkrypt0nn/Python-Discord-Bot-Template/issues/new/choose). Once that is done, you can create a pull request for the issue. 40 | 41 | ## License 42 | 43 | Your submissions are understood to be under the same [Apache License 2.0](https://github.com/kkrypt0nn/Python-Discord-Bot-Template/blob/main/LICENSE.md) that covers the project. 44 | -------------------------------------------------------------------------------- /.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 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 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 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # pytype static type analyzer 135 | .pytype/ 136 | 137 | # Cython debug symbols 138 | cython_debug/ 139 | 140 | # PyCharm IDEA 141 | .idea/* 142 | 143 | # SQLITE database 144 | *.db 145 | 146 | # Log file 147 | discord.log -------------------------------------------------------------------------------- /database/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright © Krypton 2019-Present - https://github.com/kkrypt0nn (https://krypton.ninja) 3 | Description: 4 | 🐍 A simple template to start to code your own and personalized Discord bot in Python 5 | 6 | Version: 6.4.0 7 | """ 8 | 9 | import aiosqlite 10 | 11 | 12 | class DatabaseManager: 13 | def __init__(self, *, connection: aiosqlite.Connection) -> None: 14 | self.connection = connection 15 | 16 | async def add_warn( 17 | self, user_id: int, server_id: int, moderator_id: int, reason: str 18 | ) -> int: 19 | """ 20 | This function will add a warn to the database. 21 | 22 | :param user_id: The ID of the user that should be warned. 23 | :param reason: The reason why the user should be warned. 24 | """ 25 | rows = await self.connection.execute( 26 | "SELECT id FROM warns WHERE user_id=? AND server_id=? ORDER BY id DESC LIMIT 1", 27 | ( 28 | user_id, 29 | server_id, 30 | ), 31 | ) 32 | async with rows as cursor: 33 | result = await cursor.fetchone() 34 | warn_id = result[0] + 1 if result is not None else 1 35 | await self.connection.execute( 36 | "INSERT INTO warns(id, user_id, server_id, moderator_id, reason) VALUES (?, ?, ?, ?, ?)", 37 | ( 38 | warn_id, 39 | user_id, 40 | server_id, 41 | moderator_id, 42 | reason, 43 | ), 44 | ) 45 | await self.connection.commit() 46 | return warn_id 47 | 48 | async def remove_warn(self, warn_id: int, user_id: int, server_id: int) -> int: 49 | """ 50 | This function will remove a warn from the database. 51 | 52 | :param warn_id: The ID of the warn. 53 | :param user_id: The ID of the user that was warned. 54 | :param server_id: The ID of the server where the user has been warned 55 | """ 56 | await self.connection.execute( 57 | "DELETE FROM warns WHERE id=? AND user_id=? AND server_id=?", 58 | ( 59 | warn_id, 60 | user_id, 61 | server_id, 62 | ), 63 | ) 64 | await self.connection.commit() 65 | rows = await self.connection.execute( 66 | "SELECT COUNT(*) FROM warns WHERE user_id=? AND server_id=?", 67 | ( 68 | user_id, 69 | server_id, 70 | ), 71 | ) 72 | async with rows as cursor: 73 | result = await cursor.fetchone() 74 | return result[0] if result is not None else 0 75 | 76 | async def get_warnings(self, user_id: int, server_id: int) -> list: 77 | """ 78 | This function will get all the warnings of a user. 79 | 80 | :param user_id: The ID of the user that should be checked. 81 | :param server_id: The ID of the server that should be checked. 82 | :return: A list of all the warnings of the user. 83 | """ 84 | rows = await self.connection.execute( 85 | "SELECT user_id, server_id, moderator_id, reason, strftime('%s', created_at), id FROM warns WHERE user_id=? AND server_id=?", 86 | ( 87 | user_id, 88 | server_id, 89 | ), 90 | ) 91 | async with rows as cursor: 92 | result = await cursor.fetchall() 93 | result_list = [] 94 | for row in result: 95 | result_list.append(row) 96 | return result_list 97 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | root@krypton.ninja. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /cogs/fun.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright © Krypton 2019-Present - https://github.com/kkrypt0nn (https://krypton.ninja) 3 | Description: 4 | 🐍 A simple template to start to code your own and personalized Discord bot in Python 5 | 6 | Version: 6.4.0 7 | """ 8 | 9 | import random 10 | 11 | import aiohttp 12 | import discord 13 | from discord.ext import commands 14 | from discord.ext.commands import Context 15 | 16 | 17 | class Choice(discord.ui.View): 18 | def __init__(self) -> None: 19 | super().__init__() 20 | self.value = None 21 | 22 | @discord.ui.button(label="Heads", style=discord.ButtonStyle.blurple) 23 | async def confirm( 24 | self, interaction: discord.Interaction, button: discord.ui.Button 25 | ) -> None: 26 | self.value = "heads" 27 | self.stop() 28 | 29 | @discord.ui.button(label="Tails", style=discord.ButtonStyle.blurple) 30 | async def cancel( 31 | self, interaction: discord.Interaction, button: discord.ui.Button 32 | ) -> None: 33 | self.value = "tails" 34 | self.stop() 35 | 36 | 37 | class RockPaperScissors(discord.ui.Select): 38 | def __init__(self) -> None: 39 | options = [ 40 | discord.SelectOption( 41 | label="Scissors", description="You choose scissors.", emoji="✂" 42 | ), 43 | discord.SelectOption( 44 | label="Rock", description="You choose rock.", emoji="🪨" 45 | ), 46 | discord.SelectOption( 47 | label="Paper", description="You choose paper.", emoji="🧻" 48 | ), 49 | ] 50 | super().__init__( 51 | placeholder="Choose...", 52 | min_values=1, 53 | max_values=1, 54 | options=options, 55 | ) 56 | 57 | async def callback(self, interaction: discord.Interaction) -> None: 58 | choices = { 59 | "rock": 0, 60 | "paper": 1, 61 | "scissors": 2, 62 | } 63 | user_choice = self.values[0].lower() 64 | user_choice_index = choices[user_choice] 65 | 66 | bot_choice = random.choice(list(choices.keys())) 67 | bot_choice_index = choices[bot_choice] 68 | 69 | result_embed = discord.Embed(color=0xBEBEFE) 70 | result_embed.set_author( 71 | name=interaction.user.name, icon_url=interaction.user.display_avatar.url 72 | ) 73 | 74 | winner = (3 + user_choice_index - bot_choice_index) % 3 75 | if winner == 0: 76 | result_embed.description = f"**That's a draw!**\nYou've chosen {user_choice} and I've chosen {bot_choice}." 77 | result_embed.colour = 0xF59E42 78 | elif winner == 1: 79 | result_embed.description = f"**You won!**\nYou've chosen {user_choice} and I've chosen {bot_choice}." 80 | result_embed.colour = 0x57F287 81 | else: 82 | result_embed.description = f"**You lost!**\nYou've chosen {user_choice} and I've chosen {bot_choice}." 83 | result_embed.colour = 0xE02B2B 84 | 85 | await interaction.response.edit_message( 86 | embed=result_embed, content=None, view=None 87 | ) 88 | 89 | 90 | class RockPaperScissorsView(discord.ui.View): 91 | def __init__(self) -> None: 92 | super().__init__() 93 | self.add_item(RockPaperScissors()) 94 | 95 | 96 | class Fun(commands.Cog, name="fun"): 97 | def __init__(self, bot) -> None: 98 | self.bot = bot 99 | 100 | @commands.hybrid_command(name="randomfact", description="Get a random fact.") 101 | async def randomfact(self, context: Context) -> None: 102 | """ 103 | Get a random fact. 104 | 105 | :param context: The hybrid command context. 106 | """ 107 | # This will prevent your bot from stopping everything when doing a web request - see: https://discordpy.readthedocs.io/en/stable/faq.html#how-do-i-make-a-web-request 108 | async with aiohttp.ClientSession() as session: 109 | async with session.get( 110 | "https://uselessfacts.jsph.pl/random.json?language=en" 111 | ) as request: 112 | if request.status == 200: 113 | data = await request.json() 114 | embed = discord.Embed(description=data["text"], color=0xD75BF4) 115 | else: 116 | embed = discord.Embed( 117 | title="Error!", 118 | description="There is something wrong with the API, please try again later", 119 | color=0xE02B2B, 120 | ) 121 | await context.send(embed=embed) 122 | 123 | @commands.hybrid_command( 124 | name="coinflip", description="Make a coin flip, but give your bet before." 125 | ) 126 | async def coinflip(self, context: Context) -> None: 127 | """ 128 | Make a coin flip, but give your bet before. 129 | 130 | :param context: The hybrid command context. 131 | """ 132 | buttons = Choice() 133 | embed = discord.Embed(description="What is your bet?", color=0xBEBEFE) 134 | message = await context.send(embed=embed, view=buttons) 135 | await buttons.wait() # We wait for the user to click a button. 136 | result = random.choice(["heads", "tails"]) 137 | if buttons.value == result: 138 | embed = discord.Embed( 139 | description=f"Correct! You guessed `{buttons.value}` and I flipped the coin to `{result}`.", 140 | color=0xBEBEFE, 141 | ) 142 | else: 143 | embed = discord.Embed( 144 | description=f"Woops! You guessed `{buttons.value}` and I flipped the coin to `{result}`, better luck next time!", 145 | color=0xE02B2B, 146 | ) 147 | await message.edit(embed=embed, view=None, content=None) 148 | 149 | @commands.hybrid_command( 150 | name="rps", description="Play the rock paper scissors game against the bot." 151 | ) 152 | async def rock_paper_scissors(self, context: Context) -> None: 153 | """ 154 | Play the rock paper scissors game against the bot. 155 | 156 | :param context: The hybrid command context. 157 | """ 158 | view = RockPaperScissorsView() 159 | await context.send("Please make your choice", view=view) 160 | 161 | 162 | async def setup(bot) -> None: 163 | await bot.add_cog(Fun(bot)) 164 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python Discord Bot Template 2 | 3 |

4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |

12 | 13 | > [!NOTE] 14 | > This project is in a **feature-freeze mode**, please read more about it [here](https://github.com/kkrypt0nn/Python-Discord-Bot-Template/issues/112). It can be summed up in a few bullet points: 15 | > 16 | > * The project **will** receive bug fixes 17 | > * The project **will** be updated to make sure it works with the **latest** discord.py version 18 | > * The project **will not** receive any new features, **unless one of the following applies**: 19 | > * A new feature is added to Discord and it would be beneficial to have it in the template 20 | > * A feature got a breaking change, this fits with the same point that the project will **always** support the latest discord.py version 21 | 22 | This repository is a template that everyone can use for the start of their Discord bot. 23 | 24 | When I first started creating my Discord bot it took me a while to get everything setup and working with cogs and more. 25 | I would've been happy if there were any template existing. However, there wasn't any existing template. That's why I 26 | decided to create my own template to let **you** guys create your Discord bot easily. 27 | 28 | Please note that this template is not supposed to be the best template, but a good template to start learning how 29 | discord.py works and to make your own bot easily. 30 | 31 | If you plan to use this template to make your own template or bot, you **have to**: 32 | 33 | - Keep the credits, and a link to this repository in all the files that contains my code 34 | - Keep the same license for unchanged code 35 | 36 | See [the license file](https://github.com/kkrypt0nn/Python-Discord-Bot-Template/blob/master/LICENSE.md) for more 37 | information, I reserve the right to take down any repository that does not meet these requirements. 38 | 39 | ## Support 40 | 41 | Before requesting support, you should know that this template requires you to have at least a **basic knowledge** of 42 | Python and the library is made for **advanced users**. Do not use this template if you don't know the 43 | basics or some advanced topics such as OOP or async. [Here's](https://pythondiscord.com/pages/resources) a link for resources to learn python. 44 | 45 | If you need some help for something, do not hesitate to create an issue over [here](https://github.com/kkrypt0nn/Python-Discord-Bot-Template/issues), but don't forget the read the [frequently asked questions](https://github.com/kkrypt0nn/Python-Discord-Bot-Template/wiki/Frequently-Asked-Questions) before. 46 | 47 | All the updates of the template are available [here](UPDATES.md). 48 | 49 | ## Disclaimer 50 | 51 | Slash commands can take some time to get registered globally, so if you want to test a command you should use 52 | the `@app_commands.guilds()` decorator so that it gets registered instantly. Example: 53 | 54 | ```py 55 | @commands.hybrid_command( 56 | name="command", 57 | description="Command description", 58 | ) 59 | @app_commands.guilds(discord.Object(id=GUILD_ID)) # Place your guild ID here 60 | ``` 61 | 62 | When using the template you confirm that you have read the [license](LICENSE.md) and comprehend that I can take down 63 | your repository if you do not meet these requirements. 64 | 65 | ## How to download it 66 | 67 | This repository is now a template, on the top left you can simply click on "**Use this template**" to create a GitHub 68 | repository based on this template. 69 | 70 | Alternatively you can do the following: 71 | 72 | - Clone/Download the repository 73 | - To clone it and get the updates you can definitely use the command 74 | `git clone` 75 | - Create a Discord bot [here](https://discord.com/developers/applications) 76 | - Get your bot token 77 | - Invite your bot on servers using the following invite: 78 | https://discord.com/oauth2/authorize?&client_id=YOUR_APPLICATION_ID_HERE&scope=bot+applications.commands&permissions=PERMISSIONS ( 79 | Replace `YOUR_APPLICATION_ID_HERE` with the application ID and replace `PERMISSIONS` with the required permissions 80 | your bot needs that it can be get at the bottom of a this 81 | page https://discord.com/developers/applications/YOUR_APPLICATION_ID_HERE/bot) 82 | 83 | ## How to set up 84 | 85 | To set up the token you will have to make use of the [`.env.example`](.env.example) file; you should rename it to `.env` and replace the `YOUR_BOT...` content with your actual values that match for your bot. 86 | 87 | Alternatively you can simply create a system environment variable with the same names and their respective value. 88 | 89 | ## How to start 90 | 91 | ### The _"usual"_ way 92 | 93 | To start the bot you simply need to launch, either your terminal (Linux, Mac & Windows), or your Command Prompt ( 94 | Windows) 95 | . 96 | 97 | Before running the bot you will need to install all the requirements with this command: 98 | 99 | ``` 100 | python -m pip install -r requirements.txt 101 | ``` 102 | 103 | After that you can start it with 104 | 105 | ``` 106 | python bot.py 107 | ``` 108 | 109 | > **Note**: You may need to replace `python` with `py`, `python3`, `python3.11`, etc. depending on what Python versions you have installed on the machine. 110 | 111 | ### Docker 112 | 113 | Support to start the bot in a Docker container has been added. After having [Docker](https://docker.com) installed on your machine, you can simply execute: 114 | 115 | ``` 116 | docker compose up -d --build 117 | ``` 118 | 119 | > **Note**: `-d` will make the container run in detached mode, so in the background. 120 | 121 | ## Issues or Questions 122 | 123 | If you have any issues or questions of how to code a specific command, you can: 124 | 125 | - Join my Discord server [here](https://discord.gg/xj6y5ZaTMr) 126 | - Post them [here](https://github.com/kkrypt0nn/Python-Discord-Bot-Template/issues) 127 | 128 | Me or other people will take their time to answer and help you. 129 | 130 | ## Versioning 131 | 132 | We use [SemVer](http://semver.org) for versioning. For the versions available, see 133 | the [tags on this repository](https://github.com/kkrypt0nn/Python-Discord-Bot-Template/tags). 134 | 135 | ## Built With 136 | 137 | - [Python 3.12.9](https://www.python.org/) 138 | 139 | ## License 140 | 141 | This project is licensed under the Apache License 2.0 - see the [LICENSE.md](LICENSE.md) file for details 142 | -------------------------------------------------------------------------------- /UPDATES.md: -------------------------------------------------------------------------------- 1 | # Updates List 2 | 3 | Here is the list of all the updates that I made on this template. 4 | 5 | ### Version 6.4.0 (12 September 2025) 6 | 7 | - Pin `discord.py` version to `2.6.3` 8 | 9 | ### Version 6.3.0 (05 March 2025) 10 | 11 | - Added support for Docker, `docker compose up` will spin up the bot and install the requirements 12 | - Pinned `discord.py` version in the `requirements.txt` file 13 | - Removed the `config.json` file and replaced the rest with `.env` just like the token 14 | 15 | ### Version 6.2.0 (21 July 2024) 16 | 17 | - Added a new `feedback` command for showcasing the use of modals (popups) 18 | - Some wording changes in the `README.md` file and the project description 19 | 20 | ### Version 6.1.0 (20 September 2023) 21 | 22 | - Various bug fixes 23 | - Added `-> None` type hint to remaining functions 24 | 25 | ### Version 6.0.1 & 6.0.2 (20 September 2023) 26 | 27 | - Added two context menu commands, one for users and one for messages 28 | 29 | ### Version 6.0.0 (19 September 2023) 30 | 31 | - Made a custom class for the bot that subclasses `commands.Bot` 32 | - Don't connect to the SQLite database all the time, created a `DatabaseManager` class that handles database operations 33 | - Changes to some Markdown files 34 | - Using environment file or environment variable for the token 35 | - Removed the custom checks, blacklist is gone & using discord.py's owner check 36 | - Changed main color from `0x9C84EF` to `0xBEBEFE` 37 | 38 | ### Version 5.5.0 (10 January 2023) 39 | 40 | - Added `bot.logger` to have proper logging 41 | - Added [`CONTRIBUTING.md`](CONTRIBUTING.md) and [`CODE_OF_CONDUCT.md`](CODE_OF_CONDUCT.md) files 42 | - Overall embeds refactoring 43 | - Issues and features templates 44 | 45 | ### Version 5.4.2 (06 January 2023) 46 | 47 | - Added `blacklist show` command to see the list of blacklisted users 48 | - `blacklist remove` and `blacklist add` commands now send proper error messages 49 | 50 | ### Version 5.4.1 (22 December 2022) 51 | 52 | - Loading files relatively to where the `bot.py` file is located, so that you can start the bot from anywhere in your system 53 | 54 | ### Version 5.4 (8 December 2022) 55 | 56 | - Added `@commands.bot_has_permissions()` checks and handle the exception for it 57 | - Fixed `purge` command 58 | - Removed `asyncio` from the requirements file 59 | 60 | ### Version 5.3 (17 October 2022) 61 | 62 | - Using `aiosqlite` instead of `sqlite3` for asynchronous database operations. 63 | 64 | ### Version 5.2.1 (04 October 2022) 65 | 66 | - Added error message when subcommands are not given 67 | - Fixed `warning remove` command 68 | - Now using keyword arguments (`async def command(self, context, *, message):`) for kick/ban reason, message to sent, etc. 69 | 70 | ### Vesion 5.2 (30 September 2022) 71 | 72 | - Added `load`, `reload` and `unload` commands. 73 | - Added `sync` and `unsync` commands. 74 | - Code refactoring and cleanup. 75 | 76 | ### Version 5.1 (12 September 2022) 77 | 78 | - Added the `help` command once again 79 | - Created a group for the `warning` command, has following sub-commands: 80 | - `add` - Adds a warning to the user 81 | - `remove` - Removes a warning from the user 82 | - `list` - Lists all the warnings of the user 83 | 84 | ### Version 5.0 (20 August 2022) 85 | 86 | > ⚠️ **Moved to discord.py 2.0 as it is now officially released** 87 | 88 | - Added `warnings` command that will show you all the warnings a user has 89 | - Moved the blacklist to `sqlite3` database 90 | - Now using **Hybrid Commands**, both prefix and slash commands will get created 91 | - When using the `warn` command, the warning will also be added in a new `sqlite3` database 92 | 93 | ### Version 4.1.1 (18 July 2022) 94 | 95 | - Fixed the custom checks not being sent in the channels correctly 96 | 97 | ### Version 4.1 (09 January 2022) 98 | 99 | - Added the `hackban` command 100 | - Separated slash commands and normal commands so that you remove one of them more easily 101 | - Moved normal commands in [`cogs/normal`](cogs/normal) 102 | - Moved slash commands in [`cogs/slash`](cogs/slash) 103 | 104 | ### Version 4.0.1 105 | 106 | - Fixed some _weird_ code 107 | 108 | ### Version 4.0 109 | 110 | - Now using [`disnake`](https://docs.disnake.dev) 111 | - Added a command that uses buttons _(coinflip)_ 112 | - Added a command that uses selection dropdown _(rps)_ 113 | - **Every** command is now in slash command **and** normal command (old way with prefix) 114 | - Make sure to **enable the message intents** for normal commands as it's now a privileged intent. 115 | - The **slash command** is **above**, the **normal command** is **below** 116 | 117 | ### Version 3.1.1 118 | 119 | - Fixed `TypeError: 'NoneType' object is not iterable` for prefix -> Python 3.10 120 | 121 | ### Version 3.1 122 | 123 | - Added a `@checks.is_owner` check which raises a `UserNotOwner` exception 124 | - Added a `@checks.not_blacklisted` check which raises a `UserBlacklisted` exception 125 | - Using checks instead of same code for every command 126 | - Various code cleanup 127 | 128 | ### Version 3.0 129 | 130 | **Now using slash commands** 131 | 132 | ### Version 2.8 133 | 134 | - Blacklisted users are now **saved** in the file 135 | - Moved config file to JSON 136 | - Moved the blacklist in a separate file (`blacklist.json`) 137 | - The colors are no longer saved in the config file 138 | 139 | ### Version 2.7 140 | 141 | - Added a check for `commands.MissingRequiredArgument` in the error handler 142 | - Added a disclaimer section in the [README](README.md) file 143 | - Added the latency of the bot in the `ping` command 144 | - Created the [TODO list](TODO.md) file 145 | - Fixed some error embeds having success (green) colors 146 | - Removed an unnecessary `self.bot.logout()` statement 147 | - Removed the `dick` command, as I want to keep this template safe for work 148 | - Renamed the names of the arguments in some commands 149 | - The bot now **tries** to send an embed in the private message of the command author for the `invite` and `server` 150 | commands, if this was not successful it will be sent in the channel 151 | 152 | ### Version 2.6 153 | 154 | - Added new `dailyfact` command that gives a random fact every day, using cool down 155 | - Fixed some typos in [README.md](README.md) 156 | - Remade the `on_command_error` event for `CommandOnCooldown` 157 | 158 | ### Version 2.5 159 | 160 | - Code reformat 161 | - Rewrote the status task 162 | - Now using the `has_permissions` decorator for user permissions 163 | - Using `.yaml` instead of `.py` file for config 164 | 165 | ### Version 2.4.3 166 | 167 | - Fixed intents for `serverinfo` command 168 | 169 | ### Version 2.4.2 170 | 171 | - Blacklisted users are being ignored when executing a command 172 | 173 | ### Version 2.4.1 174 | 175 | - Added config import to moderation cog 176 | 177 | ### Version 2.4 178 | 179 | - Added some fun commands 180 | - Colors are saved in the [config file](config.json) for easier usage 181 | - Cogs are now being loaded automatically 182 | - Fixed some typos 183 | 184 | ### Version 2.3 185 | 186 | - Made the kick command actually kick 187 | - Added a template cog to create cogs easily 188 | 189 | ### Version 2.2 190 | 191 | - Fixed the purge command 192 | - Made the error embeds actually red... 193 | 194 | ### Version 2.1 195 | 196 | - Made the help command dynamic 197 | - Added a small description to all commands 198 | - Added intents when creating the bot 199 | 200 | ### Version 2.0 201 | 202 | - Added cogs 203 | - Added f-strings and removed `.format()` 204 | - Created [config file](config.json) for easier setup 205 | 206 | ### Version 1.2 207 | 208 | - Added blacklist command 209 | - Removed commands cool down 210 | -------------------------------------------------------------------------------- /cogs/owner.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright © Krypton 2019-Present - https://github.com/kkrypt0nn (https://krypton.ninja) 3 | Description: 4 | 🐍 A simple template to start to code your own and personalized Discord bot in Python 5 | 6 | Version: 6.4.0 7 | """ 8 | 9 | import discord 10 | from discord import app_commands 11 | from discord.ext import commands 12 | from discord.ext.commands import Context 13 | 14 | 15 | class Owner(commands.Cog, name="owner"): 16 | def __init__(self, bot) -> None: 17 | self.bot = bot 18 | 19 | @commands.command( 20 | name="sync", 21 | description="Synchonizes the slash commands.", 22 | ) 23 | @app_commands.describe(scope="The scope of the sync. Can be `global` or `guild`") 24 | @commands.is_owner() 25 | async def sync(self, context: Context, scope: str) -> None: 26 | """ 27 | Synchonizes the slash commands. 28 | 29 | :param context: The command context. 30 | :param scope: The scope of the sync. Can be `global` or `guild`. 31 | """ 32 | 33 | if scope == "global": 34 | await context.bot.tree.sync() 35 | embed = discord.Embed( 36 | description="Slash commands have been globally synchronized.", 37 | color=0xBEBEFE, 38 | ) 39 | await context.send(embed=embed) 40 | return 41 | elif scope == "guild": 42 | context.bot.tree.copy_global_to(guild=context.guild) 43 | await context.bot.tree.sync(guild=context.guild) 44 | embed = discord.Embed( 45 | description="Slash commands have been synchronized in this guild.", 46 | color=0xBEBEFE, 47 | ) 48 | await context.send(embed=embed) 49 | return 50 | embed = discord.Embed( 51 | description="The scope must be `global` or `guild`.", color=0xE02B2B 52 | ) 53 | await context.send(embed=embed) 54 | 55 | @commands.command( 56 | name="unsync", 57 | description="Unsynchonizes the slash commands.", 58 | ) 59 | @app_commands.describe( 60 | scope="The scope of the sync. Can be `global`, `current_guild` or `guild`" 61 | ) 62 | @commands.is_owner() 63 | async def unsync(self, context: Context, scope: str) -> None: 64 | """ 65 | Unsynchonizes the slash commands. 66 | 67 | :param context: The command context. 68 | :param scope: The scope of the sync. Can be `global`, `current_guild` or `guild`. 69 | """ 70 | 71 | if scope == "global": 72 | context.bot.tree.clear_commands(guild=None) 73 | await context.bot.tree.sync() 74 | embed = discord.Embed( 75 | description="Slash commands have been globally unsynchronized.", 76 | color=0xBEBEFE, 77 | ) 78 | await context.send(embed=embed) 79 | return 80 | elif scope == "guild": 81 | context.bot.tree.clear_commands(guild=context.guild) 82 | await context.bot.tree.sync(guild=context.guild) 83 | embed = discord.Embed( 84 | description="Slash commands have been unsynchronized in this guild.", 85 | color=0xBEBEFE, 86 | ) 87 | await context.send(embed=embed) 88 | return 89 | embed = discord.Embed( 90 | description="The scope must be `global` or `guild`.", color=0xE02B2B 91 | ) 92 | await context.send(embed=embed) 93 | 94 | @commands.hybrid_command( 95 | name="load", 96 | description="Load a cog", 97 | ) 98 | @app_commands.describe(cog="The name of the cog to load") 99 | @commands.is_owner() 100 | async def load(self, context: Context, cog: str) -> None: 101 | """ 102 | The bot will load the given cog. 103 | 104 | :param context: The hybrid command context. 105 | :param cog: The name of the cog to load. 106 | """ 107 | try: 108 | await self.bot.load_extension(f"cogs.{cog}") 109 | except Exception: 110 | embed = discord.Embed( 111 | description=f"Could not load the `{cog}` cog.", color=0xE02B2B 112 | ) 113 | await context.send(embed=embed) 114 | return 115 | embed = discord.Embed( 116 | description=f"Successfully loaded the `{cog}` cog.", color=0xBEBEFE 117 | ) 118 | await context.send(embed=embed) 119 | 120 | @commands.hybrid_command( 121 | name="unload", 122 | description="Unloads a cog.", 123 | ) 124 | @app_commands.describe(cog="The name of the cog to unload") 125 | @commands.is_owner() 126 | async def unload(self, context: Context, cog: str) -> None: 127 | """ 128 | The bot will unload the given cog. 129 | 130 | :param context: The hybrid command context. 131 | :param cog: The name of the cog to unload. 132 | """ 133 | try: 134 | await self.bot.unload_extension(f"cogs.{cog}") 135 | except Exception: 136 | embed = discord.Embed( 137 | description=f"Could not unload the `{cog}` cog.", color=0xE02B2B 138 | ) 139 | await context.send(embed=embed) 140 | return 141 | embed = discord.Embed( 142 | description=f"Successfully unloaded the `{cog}` cog.", color=0xBEBEFE 143 | ) 144 | await context.send(embed=embed) 145 | 146 | @commands.hybrid_command( 147 | name="reload", 148 | description="Reloads a cog.", 149 | ) 150 | @app_commands.describe(cog="The name of the cog to reload") 151 | @commands.is_owner() 152 | async def reload(self, context: Context, cog: str) -> None: 153 | """ 154 | The bot will reload the given cog. 155 | 156 | :param context: The hybrid command context. 157 | :param cog: The name of the cog to reload. 158 | """ 159 | try: 160 | await self.bot.reload_extension(f"cogs.{cog}") 161 | except Exception: 162 | embed = discord.Embed( 163 | description=f"Could not reload the `{cog}` cog.", color=0xE02B2B 164 | ) 165 | await context.send(embed=embed) 166 | return 167 | embed = discord.Embed( 168 | description=f"Successfully reloaded the `{cog}` cog.", color=0xBEBEFE 169 | ) 170 | await context.send(embed=embed) 171 | 172 | @commands.hybrid_command( 173 | name="shutdown", 174 | description="Make the bot shutdown.", 175 | ) 176 | @commands.is_owner() 177 | async def shutdown(self, context: Context) -> None: 178 | """ 179 | Shuts down the bot. 180 | 181 | :param context: The hybrid command context. 182 | """ 183 | embed = discord.Embed(description="Shutting down. Bye! :wave:", color=0xBEBEFE) 184 | await context.send(embed=embed) 185 | await self.bot.close() 186 | 187 | @commands.hybrid_command( 188 | name="say", 189 | description="The bot will say anything you want.", 190 | ) 191 | @app_commands.describe(message="The message that should be repeated by the bot") 192 | @commands.is_owner() 193 | async def say(self, context: Context, *, message: str) -> None: 194 | """ 195 | The bot will say anything you want. 196 | 197 | :param context: The hybrid command context. 198 | :param message: The message that should be repeated by the bot. 199 | """ 200 | await context.send(message) 201 | 202 | @commands.hybrid_command( 203 | name="embed", 204 | description="The bot will say anything you want, but within embeds.", 205 | ) 206 | @app_commands.describe(message="The message that should be repeated by the bot") 207 | @commands.is_owner() 208 | async def embed(self, context: Context, *, message: str) -> None: 209 | """ 210 | The bot will say anything you want, but using embeds. 211 | 212 | :param context: The hybrid command context. 213 | :param message: The message that should be repeated by the bot. 214 | """ 215 | embed = discord.Embed(description=message, color=0xBEBEFE) 216 | await context.send(embed=embed) 217 | 218 | 219 | async def setup(bot) -> None: 220 | await bot.add_cog(Owner(bot)) 221 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 10 | through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. 13 | 14 | "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or 15 | are under common control with that entity. For the purposes of this definition, 16 | "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by 17 | contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) 18 | beneficial ownership of such entity. 19 | 20 | "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. 21 | 22 | "Source" form shall mean the preferred form for making modifications, including but not limited to software source 23 | code, documentation source, and configuration files. 24 | 25 | "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including 26 | but not limited to compiled object code, generated documentation, and conversions to other media types. 27 | 28 | "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as 29 | indicated by a copyright notice that is included in or attached to the work 30 | (an example is provided in the Appendix below). 31 | 32 | "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work 33 | and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an 34 | original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain 35 | separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. 36 | 37 | "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or 38 | additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the 39 | Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright 40 | owner. For the purposes of this definition, "submitted" 41 | means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including 42 | but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems 43 | that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but 44 | excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as " 45 | Not a Contribution." 46 | 47 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been 48 | received by Licensor and subsequently incorporated within the Work. 49 | 50 | 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to 51 | You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, 52 | prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such 53 | Derivative Works in Source or Object form. 54 | 55 | 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a 56 | perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable 57 | (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise 58 | transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are 59 | necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) 60 | with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity ( 61 | including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within 62 | the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this 63 | License for that Work shall terminate as of the date such litigation is filed. 64 | 65 | 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with 66 | or without modifications, and in Source or Object form, provided that You meet the following conditions: 67 | 68 | (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and 69 | 70 | (b) You must cause any modified files to carry prominent notices stating that You changed the files; and 71 | 72 | (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, 73 | trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to 74 | any part of the Derivative Works; and 75 | 76 | (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You 77 | distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those 78 | notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a 79 | NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided 80 | along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such 81 | third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not 82 | modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside 83 | or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be 84 | construed as modifying the License. 85 | 86 | You may add Your own copyright statement to Your modifications and may provide additional or different license terms 87 | and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a 88 | whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in 89 | this License. 90 | 91 | 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for 92 | inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any 93 | additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any 94 | separate license agreement you may have executed with Licensor regarding such Contributions. 95 | 96 | 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product 97 | names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and 98 | reproducing the content of the NOTICE file. 99 | 100 | 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and 101 | each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 102 | either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, 103 | MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness 104 | of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this 105 | License. 106 | 107 | 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or 108 | otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, 109 | shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or 110 | consequential damages of any character arising as a result of this License or out of the use or inability to use the 111 | Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or 112 | any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such 113 | damages. 114 | 115 | 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose 116 | to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or 117 | rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and 118 | on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and 119 | hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason 120 | of your accepting any such warranty or additional liability. 121 | 122 | END OF TERMS AND CONDITIONS 123 | 124 | APPENDIX: How to apply the Apache License to your work. 125 | 126 | To apply the Apache License to your work, attach the following 127 | boilerplate notice, with the fields enclosed by brackets "[]" 128 | replaced with your own identifying information. (Don't include 129 | the brackets!) The text should be enclosed in the appropriate 130 | comment syntax for the file format. We also recommend that a 131 | file or class name and description of purpose be included on the 132 | same "printed page" as the copyright notice for easier 133 | identification within third-party archives. 134 | 135 | Copyright 2021 Krypton 136 | 137 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the 138 | License. You may obtain a copy of the License at 139 | 140 | http://www.apache.org/licenses/LICENSE-2.0 141 | 142 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an " 143 | AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific 144 | language governing permissions and limitations under the License. 145 | -------------------------------------------------------------------------------- /bot.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright © Krypton 2019-Present - https://github.com/kkrypt0nn (https://krypton.ninja) 3 | Description: 4 | 🐍 A simple template to start to code your own and personalized Discord bot in Python 5 | 6 | Version: 6.4.0 7 | """ 8 | 9 | import json 10 | import logging 11 | import os 12 | import platform 13 | import random 14 | import sys 15 | 16 | import aiosqlite 17 | import discord 18 | from discord.ext import commands, tasks 19 | from discord.ext.commands import Context 20 | from dotenv import load_dotenv 21 | 22 | from database import DatabaseManager 23 | 24 | load_dotenv() 25 | 26 | """ 27 | Setup bot intents (events restrictions) 28 | For more information about intents, please go to the following websites: 29 | https://discordpy.readthedocs.io/en/latest/intents.html 30 | https://discordpy.readthedocs.io/en/latest/intents.html#privileged-intents 31 | 32 | 33 | Default Intents: 34 | intents.bans = True 35 | intents.dm_messages = True 36 | intents.dm_reactions = True 37 | intents.dm_typing = True 38 | intents.emojis = True 39 | intents.emojis_and_stickers = True 40 | intents.guild_messages = True 41 | intents.guild_reactions = True 42 | intents.guild_scheduled_events = True 43 | intents.guild_typing = True 44 | intents.guilds = True 45 | intents.integrations = True 46 | intents.invites = True 47 | intents.messages = True # `message_content` is required to get the content of the messages 48 | intents.reactions = True 49 | intents.typing = True 50 | intents.voice_states = True 51 | intents.webhooks = True 52 | 53 | Privileged Intents (Needs to be enabled on developer portal of Discord), please use them only if you need them: 54 | intents.members = True 55 | intents.message_content = True 56 | intents.presences = True 57 | """ 58 | 59 | intents = discord.Intents.default() 60 | 61 | """ 62 | Uncomment this if you want to use prefix (normal) commands. 63 | It is recommended to use slash commands and therefore not use prefix commands. 64 | 65 | If you want to use prefix commands, make sure to also enable the intent below in the Discord developer portal. 66 | """ 67 | # intents.message_content = True 68 | 69 | # Setup both of the loggers 70 | 71 | 72 | class LoggingFormatter(logging.Formatter): 73 | # Colors 74 | black = "\x1b[30m" 75 | red = "\x1b[31m" 76 | green = "\x1b[32m" 77 | yellow = "\x1b[33m" 78 | blue = "\x1b[34m" 79 | gray = "\x1b[38m" 80 | # Styles 81 | reset = "\x1b[0m" 82 | bold = "\x1b[1m" 83 | 84 | COLORS = { 85 | logging.DEBUG: gray + bold, 86 | logging.INFO: blue + bold, 87 | logging.WARNING: yellow + bold, 88 | logging.ERROR: red, 89 | logging.CRITICAL: red + bold, 90 | } 91 | 92 | def format(self, record): 93 | log_color = self.COLORS[record.levelno] 94 | format = "(black){asctime}(reset) (levelcolor){levelname:<8}(reset) (green){name}(reset) {message}" 95 | format = format.replace("(black)", self.black + self.bold) 96 | format = format.replace("(reset)", self.reset) 97 | format = format.replace("(levelcolor)", log_color) 98 | format = format.replace("(green)", self.green + self.bold) 99 | formatter = logging.Formatter(format, "%Y-%m-%d %H:%M:%S", style="{") 100 | return formatter.format(record) 101 | 102 | 103 | logger = logging.getLogger("discord_bot") 104 | logger.setLevel(logging.INFO) 105 | 106 | # Console handler 107 | console_handler = logging.StreamHandler() 108 | console_handler.setFormatter(LoggingFormatter()) 109 | # File handler 110 | file_handler = logging.FileHandler(filename="discord.log", encoding="utf-8", mode="w") 111 | file_handler_formatter = logging.Formatter( 112 | "[{asctime}] [{levelname:<8}] {name}: {message}", "%Y-%m-%d %H:%M:%S", style="{" 113 | ) 114 | file_handler.setFormatter(file_handler_formatter) 115 | 116 | # Add the handlers 117 | logger.addHandler(console_handler) 118 | logger.addHandler(file_handler) 119 | 120 | 121 | class DiscordBot(commands.Bot): 122 | def __init__(self) -> None: 123 | super().__init__( 124 | command_prefix=commands.when_mentioned_or(os.getenv("PREFIX")), 125 | intents=intents, 126 | help_command=None, 127 | ) 128 | """ 129 | This creates custom bot variables so that we can access these variables in cogs more easily. 130 | 131 | For example, The logger is available using the following code: 132 | - self.logger # In this class 133 | - bot.logger # In this file 134 | - self.bot.logger # In cogs 135 | """ 136 | self.logger = logger 137 | self.database = None 138 | self.bot_prefix = os.getenv("PREFIX") 139 | self.invite_link = os.getenv("INVITE_LINK") 140 | 141 | async def init_db(self) -> None: 142 | async with aiosqlite.connect( 143 | f"{os.path.realpath(os.path.dirname(__file__))}/database/database.db" 144 | ) as db: 145 | with open( 146 | f"{os.path.realpath(os.path.dirname(__file__))}/database/schema.sql", 147 | encoding = "utf-8" 148 | ) as file: 149 | await db.executescript(file.read()) 150 | await db.commit() 151 | 152 | async def load_cogs(self) -> None: 153 | """ 154 | The code in this function is executed whenever the bot will start. 155 | """ 156 | for file in os.listdir(f"{os.path.realpath(os.path.dirname(__file__))}/cogs"): 157 | if file.endswith(".py"): 158 | extension = file[:-3] 159 | try: 160 | await self.load_extension(f"cogs.{extension}") 161 | self.logger.info(f"Loaded extension '{extension}'") 162 | except Exception as e: 163 | exception = f"{type(e).__name__}: {e}" 164 | self.logger.error( 165 | f"Failed to load extension {extension}\n{exception}" 166 | ) 167 | 168 | @tasks.loop(minutes=1.0) 169 | async def status_task(self) -> None: 170 | """ 171 | Setup the game status task of the bot. 172 | """ 173 | statuses = ["with you!", "with Krypton!", "with humans!"] 174 | await self.change_presence(activity=discord.Game(random.choice(statuses))) 175 | 176 | @status_task.before_loop 177 | async def before_status_task(self) -> None: 178 | """ 179 | Before starting the status changing task, we make sure the bot is ready 180 | """ 181 | await self.wait_until_ready() 182 | 183 | async def setup_hook(self) -> None: 184 | """ 185 | This will just be executed when the bot starts the first time. 186 | """ 187 | self.logger.info(f"Logged in as {self.user.name}") 188 | self.logger.info(f"discord.py API version: {discord.__version__}") 189 | self.logger.info(f"Python version: {platform.python_version()}") 190 | self.logger.info( 191 | f"Running on: {platform.system()} {platform.release()} ({os.name})" 192 | ) 193 | self.logger.info("-------------------") 194 | await self.init_db() 195 | await self.load_cogs() 196 | self.status_task.start() 197 | self.database = DatabaseManager( 198 | connection=await aiosqlite.connect( 199 | f"{os.path.realpath(os.path.dirname(__file__))}/database/database.db" 200 | ) 201 | ) 202 | 203 | async def on_message(self, message: discord.Message) -> None: 204 | """ 205 | The code in this event is executed every time someone sends a message, with or without the prefix 206 | 207 | :param message: The message that was sent. 208 | """ 209 | if message.author == self.user or message.author.bot: 210 | return 211 | await self.process_commands(message) 212 | 213 | async def on_command_completion(self, context: Context) -> None: 214 | """ 215 | The code in this event is executed every time a normal command has been *successfully* executed. 216 | 217 | :param context: The context of the command that has been executed. 218 | """ 219 | full_command_name = context.command.qualified_name 220 | split = full_command_name.split(" ") 221 | executed_command = str(split[0]) 222 | if context.guild is not None: 223 | self.logger.info( 224 | f"Executed {executed_command} command in {context.guild.name} (ID: {context.guild.id}) by {context.author} (ID: {context.author.id})" 225 | ) 226 | else: 227 | self.logger.info( 228 | f"Executed {executed_command} command by {context.author} (ID: {context.author.id}) in DMs" 229 | ) 230 | 231 | async def on_command_error(self, context: Context, error) -> None: 232 | """ 233 | The code in this event is executed every time a normal valid command catches an error. 234 | 235 | :param context: The context of the normal command that failed executing. 236 | :param error: The error that has been faced. 237 | """ 238 | if isinstance(error, commands.CommandOnCooldown): 239 | minutes, seconds = divmod(error.retry_after, 60) 240 | hours, minutes = divmod(minutes, 60) 241 | hours = hours % 24 242 | embed = discord.Embed( 243 | description=f"**Please slow down** - You can use this command again in {f'{round(hours)} hours' if round(hours) > 0 else ''} {f'{round(minutes)} minutes' if round(minutes) > 0 else ''} {f'{round(seconds)} seconds' if round(seconds) > 0 else ''}.", 244 | color=0xE02B2B, 245 | ) 246 | await context.send(embed=embed) 247 | elif isinstance(error, commands.NotOwner): 248 | embed = discord.Embed( 249 | description="You are not the owner of the bot!", color=0xE02B2B 250 | ) 251 | await context.send(embed=embed) 252 | if context.guild: 253 | self.logger.warning( 254 | f"{context.author} (ID: {context.author.id}) tried to execute an owner only command in the guild {context.guild.name} (ID: {context.guild.id}), but the user is not an owner of the bot." 255 | ) 256 | else: 257 | self.logger.warning( 258 | f"{context.author} (ID: {context.author.id}) tried to execute an owner only command in the bot's DMs, but the user is not an owner of the bot." 259 | ) 260 | elif isinstance(error, commands.MissingPermissions): 261 | embed = discord.Embed( 262 | description="You are missing the permission(s) `" 263 | + ", ".join(error.missing_permissions) 264 | + "` to execute this command!", 265 | color=0xE02B2B, 266 | ) 267 | await context.send(embed=embed) 268 | elif isinstance(error, commands.BotMissingPermissions): 269 | embed = discord.Embed( 270 | description="I am missing the permission(s) `" 271 | + ", ".join(error.missing_permissions) 272 | + "` to fully perform this command!", 273 | color=0xE02B2B, 274 | ) 275 | await context.send(embed=embed) 276 | elif isinstance(error, commands.MissingRequiredArgument): 277 | embed = discord.Embed( 278 | title="Error!", 279 | # We need to capitalize because the command arguments have no capital letter in the code and they are the first word in the error message. 280 | description=str(error).capitalize(), 281 | color=0xE02B2B, 282 | ) 283 | await context.send(embed=embed) 284 | else: 285 | raise error 286 | 287 | 288 | bot = DiscordBot() 289 | bot.run(os.getenv("TOKEN")) 290 | -------------------------------------------------------------------------------- /cogs/general.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright © Krypton 2019-Present - https://github.com/kkrypt0nn (https://krypton.ninja) 3 | Description: 4 | 🐍 A simple template to start to code your own and personalized Discord bot in Python 5 | 6 | Version: 6.4.0 7 | """ 8 | 9 | import platform 10 | import random 11 | 12 | import aiohttp 13 | import discord 14 | from discord import app_commands 15 | from discord.ext import commands 16 | from discord.ext.commands import Context 17 | 18 | 19 | class FeedbackForm(discord.ui.Modal, title="Feeedback"): 20 | feedback = discord.ui.TextInput( 21 | label="What do you think about this bot?", 22 | style=discord.TextStyle.long, 23 | placeholder="Type your answer here...", 24 | required=True, 25 | max_length=256, 26 | ) 27 | 28 | async def on_submit(self, interaction: discord.Interaction): 29 | self.interaction = interaction 30 | self.answer = str(self.feedback) 31 | self.stop() 32 | 33 | 34 | class General(commands.Cog, name="general"): 35 | def __init__(self, bot) -> None: 36 | self.bot = bot 37 | self.context_menu_user = app_commands.ContextMenu( 38 | name="Grab ID", callback=self.grab_id 39 | ) 40 | self.bot.tree.add_command(self.context_menu_user) 41 | self.context_menu_message = app_commands.ContextMenu( 42 | name="Remove spoilers", callback=self.remove_spoilers 43 | ) 44 | self.bot.tree.add_command(self.context_menu_message) 45 | 46 | # Message context menu command 47 | async def remove_spoilers( 48 | self, interaction: discord.Interaction, message: discord.Message 49 | ) -> None: 50 | """ 51 | Removes the spoilers from the message. This command requires the MESSAGE_CONTENT intent to work properly. 52 | 53 | :param interaction: The application command interaction. 54 | :param message: The message that is being interacted with. 55 | """ 56 | spoiler_attachment = None 57 | for attachment in message.attachments: 58 | if attachment.is_spoiler(): 59 | spoiler_attachment = attachment 60 | break 61 | embed = discord.Embed( 62 | title="Message without spoilers", 63 | description=message.content.replace("||", ""), 64 | color=0xBEBEFE, 65 | ) 66 | if spoiler_attachment is not None: 67 | embed.set_image(url=attachment.url) 68 | await interaction.response.send_message(embed=embed, ephemeral=True) 69 | 70 | # User context menu command 71 | async def grab_id( 72 | self, interaction: discord.Interaction, user: discord.User 73 | ) -> None: 74 | """ 75 | Grabs the ID of the user. 76 | 77 | :param interaction: The application command interaction. 78 | :param user: The user that is being interacted with. 79 | """ 80 | embed = discord.Embed( 81 | description=f"The ID of {user.mention} is `{user.id}`.", 82 | color=0xBEBEFE, 83 | ) 84 | await interaction.response.send_message(embed=embed, ephemeral=True) 85 | 86 | @commands.hybrid_command( 87 | name="help", description="List all commands the bot has loaded." 88 | ) 89 | async def help(self, context: Context) -> None: 90 | embed = discord.Embed( 91 | title="Help", description="List of available commands:", color=0xBEBEFE 92 | ) 93 | for i in self.bot.cogs: 94 | if i == "owner" and not (await self.bot.is_owner(context.author)): 95 | continue 96 | cog = self.bot.get_cog(i.lower()) 97 | commands = cog.get_commands() 98 | data = [] 99 | for command in commands: 100 | description = command.description.partition("\n")[0] 101 | data.append(f"{command.name} - {description}") 102 | help_text = "\n".join(data) 103 | embed.add_field( 104 | name=i.capitalize(), value=f"```{help_text}```", inline=False 105 | ) 106 | await context.send(embed=embed) 107 | 108 | @commands.hybrid_command( 109 | name="botinfo", 110 | description="Get some useful (or not) information about the bot.", 111 | ) 112 | async def botinfo(self, context: Context) -> None: 113 | """ 114 | Get some useful (or not) information about the bot. 115 | 116 | :param context: The hybrid command context. 117 | """ 118 | embed = discord.Embed( 119 | description="Used [Krypton's](https://krypton.ninja) template", 120 | color=0xBEBEFE, 121 | ) 122 | embed.set_author(name="Bot Information") 123 | embed.add_field(name="Owner:", value="Krypton#7331", inline=True) 124 | embed.add_field( 125 | name="Python Version:", value=f"{platform.python_version()}", inline=True 126 | ) 127 | embed.add_field( 128 | name="Prefix:", 129 | value=f"/ (Slash Commands) or {self.bot.bot_prefix} for normal commands", 130 | inline=False, 131 | ) 132 | embed.set_footer(text=f"Requested by {context.author}") 133 | await context.send(embed=embed) 134 | 135 | @commands.hybrid_command( 136 | name="serverinfo", 137 | description="Get some useful (or not) information about the server.", 138 | ) 139 | async def serverinfo(self, context: Context) -> None: 140 | """ 141 | Get some useful (or not) information about the server. 142 | 143 | :param context: The hybrid command context. 144 | """ 145 | roles = [role.name for role in context.guild.roles] 146 | num_roles = len(roles) 147 | if num_roles > 50: 148 | roles = roles[:50] 149 | roles.append(f">>>> Displaying [50/{num_roles}] Roles") 150 | roles = ", ".join(roles) 151 | 152 | embed = discord.Embed( 153 | title="**Server Name:**", description=f"{context.guild}", color=0xBEBEFE 154 | ) 155 | if context.guild.icon is not None: 156 | embed.set_thumbnail(url=context.guild.icon.url) 157 | embed.add_field(name="Server ID", value=context.guild.id) 158 | embed.add_field(name="Member Count", value=context.guild.member_count) 159 | embed.add_field( 160 | name="Text/Voice Channels", value=f"{len(context.guild.channels)}" 161 | ) 162 | embed.add_field(name=f"Roles ({len(context.guild.roles)})", value=roles) 163 | embed.set_footer(text=f"Created at: {context.guild.created_at}") 164 | await context.send(embed=embed) 165 | 166 | @commands.hybrid_command( 167 | name="ping", 168 | description="Check if the bot is alive.", 169 | ) 170 | async def ping(self, context: Context) -> None: 171 | """ 172 | Check if the bot is alive. 173 | 174 | :param context: The hybrid command context. 175 | """ 176 | embed = discord.Embed( 177 | title="🏓 Pong!", 178 | description=f"The bot latency is {round(self.bot.latency * 1000)}ms.", 179 | color=0xBEBEFE, 180 | ) 181 | await context.send(embed=embed) 182 | 183 | @commands.hybrid_command( 184 | name="invite", 185 | description="Get the invite link of the bot to be able to invite it.", 186 | ) 187 | async def invite(self, context: Context) -> None: 188 | """ 189 | Get the invite link of the bot to be able to invite it. 190 | 191 | :param context: The hybrid command context. 192 | """ 193 | embed = discord.Embed( 194 | description=f"Invite me by clicking [here]({self.bot.invite_link}).", 195 | color=0xD75BF4, 196 | ) 197 | try: 198 | await context.author.send(embed=embed) 199 | await context.send("I sent you a private message!") 200 | except discord.Forbidden: 201 | await context.send(embed=embed) 202 | 203 | @commands.hybrid_command( 204 | name="server", 205 | description="Get the invite link of the discord server of the bot for some support.", 206 | ) 207 | async def server(self, context: Context) -> None: 208 | """ 209 | Get the invite link of the discord server of the bot for some support. 210 | 211 | :param context: The hybrid command context. 212 | """ 213 | embed = discord.Embed( 214 | description=f"Join the support server for the bot by clicking [here](https://discord.gg/mTBrXyWxAF).", 215 | color=0xD75BF4, 216 | ) 217 | try: 218 | await context.author.send(embed=embed) 219 | await context.send("I sent you a private message!") 220 | except discord.Forbidden: 221 | await context.send(embed=embed) 222 | 223 | @commands.hybrid_command( 224 | name="8ball", 225 | description="Ask any question to the bot.", 226 | ) 227 | @app_commands.describe(question="The question you want to ask.") 228 | async def eight_ball(self, context: Context, *, question: str) -> None: 229 | """ 230 | Ask any question to the bot. 231 | 232 | :param context: The hybrid command context. 233 | :param question: The question that should be asked by the user. 234 | """ 235 | answers = [ 236 | "It is certain.", 237 | "It is decidedly so.", 238 | "You may rely on it.", 239 | "Without a doubt.", 240 | "Yes - definitely.", 241 | "As I see, yes.", 242 | "Most likely.", 243 | "Outlook good.", 244 | "Yes.", 245 | "Signs point to yes.", 246 | "Reply hazy, try again.", 247 | "Ask again later.", 248 | "Better not tell you now.", 249 | "Cannot predict now.", 250 | "Concentrate and ask again later.", 251 | "Don't count on it.", 252 | "My reply is no.", 253 | "My sources say no.", 254 | "Outlook not so good.", 255 | "Very doubtful.", 256 | ] 257 | embed = discord.Embed( 258 | title="**My Answer:**", 259 | description=f"{random.choice(answers)}", 260 | color=0xBEBEFE, 261 | ) 262 | embed.set_footer(text=f"The question was: {question}") 263 | await context.send(embed=embed) 264 | 265 | @commands.hybrid_command( 266 | name="bitcoin", 267 | description="Get the current price of bitcoin.", 268 | ) 269 | async def bitcoin(self, context: Context) -> None: 270 | """ 271 | Get the current price of bitcoin. 272 | 273 | :param context: The hybrid command context. 274 | """ 275 | # This will prevent your bot from stopping everything when doing a web request - see: https://discordpy.readthedocs.io/en/stable/faq.html#how-do-i-make-a-web-request 276 | async with aiohttp.ClientSession() as session: 277 | async with session.get( 278 | "https://api.coindesk.com/v1/bpi/currentprice/BTC.json" 279 | ) as request: 280 | if request.status == 200: 281 | data = await request.json() 282 | embed = discord.Embed( 283 | title="Bitcoin price", 284 | description=f"The current price is {data['bpi']['USD']['rate']} :dollar:", 285 | color=0xBEBEFE, 286 | ) 287 | else: 288 | embed = discord.Embed( 289 | title="Error!", 290 | description="There is something wrong with the API, please try again later", 291 | color=0xE02B2B, 292 | ) 293 | await context.send(embed=embed) 294 | 295 | @app_commands.command( 296 | name="feedback", description="Submit a feedback for the owners of the bot" 297 | ) 298 | async def feedback(self, interaction: discord.Interaction) -> None: 299 | """ 300 | Submit a feedback for the owners of the bot. 301 | 302 | :param context: The hybrid command context. 303 | """ 304 | feedback_form = FeedbackForm() 305 | await interaction.response.send_modal(feedback_form) 306 | 307 | await feedback_form.wait() 308 | interaction = feedback_form.interaction 309 | await interaction.response.send_message( 310 | embed=discord.Embed( 311 | description="Thank you for your feedback, the owners have been notified about it.", 312 | color=0xBEBEFE, 313 | ) 314 | ) 315 | 316 | app_owner = (await self.bot.application_info()).owner 317 | await app_owner.send( 318 | embed=discord.Embed( 319 | title="New Feedback", 320 | description=f"{interaction.user} (<@{interaction.user.id}>) has submitted a new feedback:\n```\n{feedback_form.answer}\n```", 321 | color=0xBEBEFE, 322 | ) 323 | ) 324 | 325 | 326 | async def setup(bot) -> None: 327 | await bot.add_cog(General(bot)) 328 | -------------------------------------------------------------------------------- /cogs/moderation.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright © Krypton 2019-Present - https://github.com/kkrypt0nn (https://krypton.ninja) 3 | Description: 4 | 🐍 A simple template to start to code your own and personalized Discord bot in Python 5 | 6 | Version: 6.4.0 7 | """ 8 | 9 | import os 10 | from datetime import datetime 11 | 12 | import discord 13 | from discord import app_commands 14 | from discord.ext import commands 15 | from discord.ext.commands import Context 16 | 17 | 18 | class Moderation(commands.Cog, name="moderation"): 19 | def __init__(self, bot) -> None: 20 | self.bot = bot 21 | 22 | @commands.hybrid_command( 23 | name="kick", 24 | description="Kick a user out of the server.", 25 | ) 26 | @commands.has_permissions(kick_members=True) 27 | @commands.bot_has_permissions(kick_members=True) 28 | @app_commands.describe( 29 | user="The user that should be kicked.", 30 | reason="The reason why the user should be kicked.", 31 | ) 32 | async def kick( 33 | self, context: Context, user: discord.User, *, reason: str = "Not specified" 34 | ) -> None: 35 | """ 36 | Kick a user out of the server. 37 | 38 | :param context: The hybrid command context. 39 | :param user: The user that should be kicked from the server. 40 | :param reason: The reason for the kick. Default is "Not specified". 41 | """ 42 | member = context.guild.get_member(user.id) or await context.guild.fetch_member( 43 | user.id 44 | ) 45 | if member.guild_permissions.administrator: 46 | embed = discord.Embed( 47 | description="User has administrator permissions.", color=0xE02B2B 48 | ) 49 | await context.send(embed=embed) 50 | else: 51 | try: 52 | embed = discord.Embed( 53 | description=f"**{member}** was kicked by **{context.author}**!", 54 | color=0xBEBEFE, 55 | ) 56 | embed.add_field(name="Reason:", value=reason) 57 | await context.send(embed=embed) 58 | try: 59 | await member.send( 60 | f"You were kicked by **{context.author}** from **{context.guild.name}**!\nReason: {reason}" 61 | ) 62 | except: 63 | # Couldn't send a message in the private messages of the user 64 | pass 65 | await member.kick(reason=reason) 66 | except: 67 | embed = discord.Embed( 68 | description="An error occurred while trying to kick the user. Make sure my role is above the role of the user you want to kick.", 69 | color=0xE02B2B, 70 | ) 71 | await context.send(embed=embed) 72 | 73 | @commands.hybrid_command( 74 | name="nick", 75 | description="Change the nickname of a user on a server.", 76 | ) 77 | @commands.has_permissions(manage_nicknames=True) 78 | @commands.bot_has_permissions(manage_nicknames=True) 79 | @app_commands.describe( 80 | user="The user that should have a new nickname.", 81 | nickname="The new nickname that should be set.", 82 | ) 83 | async def nick( 84 | self, context: Context, user: discord.User, *, nickname: str = None 85 | ) -> None: 86 | """ 87 | Change the nickname of a user on a server. 88 | 89 | :param context: The hybrid command context. 90 | :param user: The user that should have its nickname changed. 91 | :param nickname: The new nickname of the user. Default is None, which will reset the nickname. 92 | """ 93 | member = context.guild.get_member(user.id) or await context.guild.fetch_member( 94 | user.id 95 | ) 96 | try: 97 | await member.edit(nick=nickname) 98 | embed = discord.Embed( 99 | description=f"**{member}'s** new nickname is **{nickname}**!", 100 | color=0xBEBEFE, 101 | ) 102 | await context.send(embed=embed) 103 | except: 104 | embed = discord.Embed( 105 | description="An error occurred while trying to change the nickname of the user. Make sure my role is above the role of the user you want to change the nickname.", 106 | color=0xE02B2B, 107 | ) 108 | await context.send(embed=embed) 109 | 110 | @commands.hybrid_command( 111 | name="ban", 112 | description="Bans a user from the server.", 113 | ) 114 | @commands.has_permissions(ban_members=True) 115 | @commands.bot_has_permissions(ban_members=True) 116 | @app_commands.describe( 117 | user="The user that should be banned.", 118 | reason="The reason why the user should be banned.", 119 | ) 120 | async def ban( 121 | self, context: Context, user: discord.User, *, reason: str = "Not specified" 122 | ) -> None: 123 | """ 124 | Bans a user from the server. 125 | 126 | :param context: The hybrid command context. 127 | :param user: The user that should be banned from the server. 128 | :param reason: The reason for the ban. Default is "Not specified". 129 | """ 130 | member = context.guild.get_member(user.id) or await context.guild.fetch_member( 131 | user.id 132 | ) 133 | try: 134 | if member.guild_permissions.administrator: 135 | embed = discord.Embed( 136 | description="User has administrator permissions.", color=0xE02B2B 137 | ) 138 | await context.send(embed=embed) 139 | else: 140 | embed = discord.Embed( 141 | description=f"**{member}** was banned by **{context.author}**!", 142 | color=0xBEBEFE, 143 | ) 144 | embed.add_field(name="Reason:", value=reason) 145 | await context.send(embed=embed) 146 | try: 147 | await member.send( 148 | f"You were banned by **{context.author}** from **{context.guild.name}**!\nReason: {reason}" 149 | ) 150 | except: 151 | # Couldn't send a message in the private messages of the user 152 | pass 153 | await member.ban(reason=reason) 154 | except: 155 | embed = discord.Embed( 156 | title="Error!", 157 | description="An error occurred while trying to ban the user. Make sure my role is above the role of the user you want to ban.", 158 | color=0xE02B2B, 159 | ) 160 | await context.send(embed=embed) 161 | 162 | @commands.hybrid_group( 163 | name="warning", 164 | description="Manage warnings of a user on a server.", 165 | ) 166 | @commands.has_permissions(manage_messages=True) 167 | async def warning(self, context: Context) -> None: 168 | """ 169 | Manage warnings of a user on a server. 170 | 171 | :param context: The hybrid command context. 172 | """ 173 | if context.invoked_subcommand is None: 174 | embed = discord.Embed( 175 | description="Please specify a subcommand.\n\n**Subcommands:**\n`add` - Add a warning to a user.\n`remove` - Remove a warning from a user.\n`list` - List all warnings of a user.", 176 | color=0xE02B2B, 177 | ) 178 | await context.send(embed=embed) 179 | 180 | @warning.command( 181 | name="add", 182 | description="Adds a warning to a user in the server.", 183 | ) 184 | @commands.has_permissions(manage_messages=True) 185 | @app_commands.describe( 186 | user="The user that should be warned.", 187 | reason="The reason why the user should be warned.", 188 | ) 189 | async def warning_add( 190 | self, context: Context, user: discord.User, *, reason: str = "Not specified" 191 | ) -> None: 192 | """ 193 | Warns a user in his private messages. 194 | 195 | :param context: The hybrid command context. 196 | :param user: The user that should be warned. 197 | :param reason: The reason for the warn. Default is "Not specified". 198 | """ 199 | member = context.guild.get_member(user.id) or await context.guild.fetch_member( 200 | user.id 201 | ) 202 | total = await self.bot.database.add_warn( 203 | user.id, context.guild.id, context.author.id, reason 204 | ) 205 | embed = discord.Embed( 206 | description=f"**{member}** was warned by **{context.author}**!\nTotal warns for this user: {total}", 207 | color=0xBEBEFE, 208 | ) 209 | embed.add_field(name="Reason:", value=reason) 210 | await context.send(embed=embed) 211 | try: 212 | await member.send( 213 | f"You were warned by **{context.author}** in **{context.guild.name}**!\nReason: {reason}" 214 | ) 215 | except: 216 | # Couldn't send a message in the private messages of the user 217 | await context.send( 218 | f"{member.mention}, you were warned by **{context.author}**!\nReason: {reason}" 219 | ) 220 | 221 | @warning.command( 222 | name="remove", 223 | description="Removes a warning from a user in the server.", 224 | ) 225 | @commands.has_permissions(manage_messages=True) 226 | @app_commands.describe( 227 | user="The user that should get their warning removed.", 228 | warn_id="The ID of the warning that should be removed.", 229 | ) 230 | async def warning_remove( 231 | self, context: Context, user: discord.User, warn_id: int 232 | ) -> None: 233 | """ 234 | Warns a user in his private messages. 235 | 236 | :param context: The hybrid command context. 237 | :param user: The user that should get their warning removed. 238 | :param warn_id: The ID of the warning that should be removed. 239 | """ 240 | member = context.guild.get_member(user.id) or await context.guild.fetch_member( 241 | user.id 242 | ) 243 | total = await self.bot.database.remove_warn(warn_id, user.id, context.guild.id) 244 | embed = discord.Embed( 245 | description=f"I've removed the warning **#{warn_id}** from **{member}**!\nTotal warns for this user: {total}", 246 | color=0xBEBEFE, 247 | ) 248 | await context.send(embed=embed) 249 | 250 | @warning.command( 251 | name="list", 252 | description="Shows the warnings of a user in the server.", 253 | ) 254 | @commands.has_guild_permissions(manage_messages=True) 255 | @app_commands.describe(user="The user you want to get the warnings of.") 256 | async def warning_list(self, context: Context, user: discord.User) -> None: 257 | """ 258 | Shows the warnings of a user in the server. 259 | 260 | :param context: The hybrid command context. 261 | :param user: The user you want to get the warnings of. 262 | """ 263 | warnings_list = await self.bot.database.get_warnings(user.id, context.guild.id) 264 | embed = discord.Embed(title=f"Warnings of {user}", color=0xBEBEFE) 265 | description = "" 266 | if len(warnings_list) == 0: 267 | description = "This user has no warnings." 268 | else: 269 | for warning in warnings_list: 270 | description += f"• Warned by <@{warning[2]}>: **{warning[3]}** () - Warn ID #{warning[5]}\n" 271 | embed.description = description 272 | await context.send(embed=embed) 273 | 274 | @commands.hybrid_command( 275 | name="purge", 276 | description="Delete a number of messages.", 277 | ) 278 | @commands.has_guild_permissions(manage_messages=True) 279 | @commands.bot_has_permissions(manage_messages=True) 280 | @app_commands.describe(amount="The amount of messages that should be deleted.") 281 | async def purge(self, context: Context, amount: int) -> None: 282 | """ 283 | Delete a number of messages. 284 | 285 | :param context: The hybrid command context. 286 | :param amount: The number of messages that should be deleted. 287 | """ 288 | await context.send( 289 | "Deleting messages..." 290 | ) # Bit of a hacky way to make sure the bot responds to the interaction and doens't get a "Unknown Interaction" response 291 | purged_messages = await context.channel.purge(limit=amount + 1) 292 | embed = discord.Embed( 293 | description=f"**{context.author}** cleared **{len(purged_messages)-1}** messages!", 294 | color=0xBEBEFE, 295 | ) 296 | await context.channel.send(embed=embed) 297 | 298 | @commands.hybrid_command( 299 | name="hackban", 300 | description="Bans a user without the user having to be in the server.", 301 | ) 302 | @commands.has_permissions(ban_members=True) 303 | @commands.bot_has_permissions(ban_members=True) 304 | @app_commands.describe( 305 | user_id="The user ID that should be banned.", 306 | reason="The reason why the user should be banned.", 307 | ) 308 | async def hackban( 309 | self, context: Context, user_id: str, *, reason: str = "Not specified" 310 | ) -> None: 311 | """ 312 | Bans a user without the user having to be in the server. 313 | 314 | :param context: The hybrid command context. 315 | :param user_id: The ID of the user that should be banned. 316 | :param reason: The reason for the ban. Default is "Not specified". 317 | """ 318 | try: 319 | await self.bot.http.ban(user_id, context.guild.id, reason=reason) 320 | user = self.bot.get_user(int(user_id)) or await self.bot.fetch_user( 321 | int(user_id) 322 | ) 323 | embed = discord.Embed( 324 | description=f"**{user}** (ID: {user_id}) was banned by **{context.author}**!", 325 | color=0xBEBEFE, 326 | ) 327 | embed.add_field(name="Reason:", value=reason) 328 | await context.send(embed=embed) 329 | except Exception: 330 | embed = discord.Embed( 331 | description="An error occurred while trying to ban the user. Make sure ID is an existing ID that belongs to a user.", 332 | color=0xE02B2B, 333 | ) 334 | await context.send(embed=embed) 335 | 336 | @commands.hybrid_command( 337 | name="archive", 338 | description="Archives in a text file the last messages with a chosen limit of messages.", 339 | ) 340 | @commands.has_permissions(manage_messages=True) 341 | @app_commands.describe( 342 | limit="The limit of messages that should be archived.", 343 | ) 344 | async def archive(self, context: Context, limit: int = 10) -> None: 345 | """ 346 | Archives in a text file the last messages with a chosen limit of messages. This command requires the MESSAGE_CONTENT intent to work properly. 347 | 348 | :param limit: The limit of messages that should be archived. Default is 10. 349 | """ 350 | log_file = f"{context.channel.id}.log" 351 | with open(log_file, "w", encoding="UTF-8") as f: 352 | f.write( 353 | f'Archived messages from: #{context.channel} ({context.channel.id}) in the guild "{context.guild}" ({context.guild.id}) at {datetime.now().strftime("%d.%m.%Y %H:%M:%S")}\n' 354 | ) 355 | async for message in context.channel.history( 356 | limit=limit, before=context.message 357 | ): 358 | attachments = [] 359 | for attachment in message.attachments: 360 | attachments.append(attachment.url) 361 | attachments_text = ( 362 | f"[Attached File{'s' if len(attachments) >= 2 else ''}: {', '.join(attachments)}]" 363 | if len(attachments) >= 1 364 | else "" 365 | ) 366 | f.write( 367 | f"{message.created_at.strftime('%d.%m.%Y %H:%M:%S')} {message.author} {message.id}: {message.clean_content} {attachments_text}\n" 368 | ) 369 | f = discord.File(log_file) 370 | await context.send(file=f) 371 | os.remove(log_file) 372 | 373 | 374 | async def setup(bot) -> None: 375 | await bot.add_cog(Moderation(bot)) 376 | --------------------------------------------------------------------------------