Initial commit

This commit is contained in:
neoarz
2025-09-14 16:09:43 -04:00
commit 6583e7ccbb
16 changed files with 1986 additions and 0 deletions

8
.dockerignore Normal file
View File

@@ -0,0 +1,8 @@
.git/
.github/
database/
venv/
.env*
CODE_OF_CONDUCT.md
CONTRIBUTING.md
UPDATES.md

3
.env.example Normal file
View File

@@ -0,0 +1,3 @@
TOKEN=YOUR_BOT_TOKEN_HERE
PREFIX=YOUR_BOT_PREFIX_HERE
INVITE_LINK=YOUR_BOT_INVITE_LINK_HERE

147
.gitignore vendored Normal file
View File

@@ -0,0 +1,147 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm IDEA
.idea/*
# SQLITE database
*.db
# Log file
discord.log

8
Dockerfile Normal file
View File

@@ -0,0 +1,8 @@
FROM python:3.12.9-slim-bookworm
WORKDIR /bot
COPY . /bot
RUN python -m pip install -r requirements.txt
ENTRYPOINT [ "python", "bot.py" ]

144
LICENSE.md Normal file
View File

@@ -0,0 +1,144 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1
through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or
are under common control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by
contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii)
beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including but not limited to software source
code, documentation source, and configuration files.
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including
but not limited to compiled object code, generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as
indicated by a copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work
and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an
original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or
additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the
Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright
owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including
but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems
that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "
Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been
received by Licensor and subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to
You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce,
prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such
Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a
perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise
transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are
necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (
including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within
the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this
License for that Work shall terminate as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with
or without modifications, and in Source or Object form, provided that You meet the following conditions:
(a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent,
trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to
any part of the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You
distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those
notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a
NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided
along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such
third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not
modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be
construed as modifying the License.
You may add Your own copyright statement to Your modifications and may provide additional or different license terms
and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a
whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in
this License.
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for
inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any
additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any
separate license agreement you may have executed with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product
names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and
reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and
each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT,
MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness
of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this
License.
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or
otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing,
shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or
consequential damages of any character arising as a result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or
any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such
damages.
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose
to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or
rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and
on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and
hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2021 Krypton
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the
License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "
AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific
language governing permissions and limitations under the License.

141
README.md Normal file
View File

@@ -0,0 +1,141 @@
# Python Discord Bot Template
<p align="center">
<a href="https://discord.gg/xj6y5ZaTMr"><img src="https://img.shields.io/discord/1358456011316396295?logo=discord"></a>
<a href="https://github.com/kkrypt0nn/Python-Discord-Bot-Template/releases"><img src="https://img.shields.io/github/v/release/kkrypt0nn/Python-Discord-Bot-Template"></a>
<a href="https://github.com/kkrypt0nn/Python-Discord-Bot-Template/commits/main"><img src="https://img.shields.io/github/last-commit/kkrypt0nn/Python-Discord-Bot-Template"></a>
<a href="https://github.com/kkrypt0nn/Python-Discord-Bot-Template/blob/main/LICENSE.md"><img src="https://img.shields.io/github/license/kkrypt0nn/Python-Discord-Bot-Template"></a>
<a href="https://github.com/kkrypt0nn/Python-Discord-Bot-Template"><img src="https://img.shields.io/github/languages/code-size/kkrypt0nn/Python-Discord-Bot-Template"></a>
<a href="https://conventionalcommits.org/en/v1.0.0/"><img src="https://img.shields.io/badge/Conventional%20Commits-1.0.0-%23FE5196?logo=conventionalcommits&logoColor=white"></a>
<a href="https://github.com/psf/black"><img src="https://img.shields.io/badge/code%20style-black-000000.svg"></a>
</p>
> [!NOTE]
> 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:
>
> * The project **will** receive bug fixes
> * The project **will** be updated to make sure it works with the **latest** discord.py version
> * The project **will not** receive any new features, **unless one of the following applies**:
> * A new feature is added to Discord and it would be beneficial to have it in the template
> * A feature got a breaking change, this fits with the same point that the project will **always** support the latest discord.py version
This repository is a template that everyone can use for the start of their Discord bot.
When I first started creating my Discord bot it took me a while to get everything setup and working with cogs and more.
I would've been happy if there were any template existing. However, there wasn't any existing template. That's why I
decided to create my own template to let **you** guys create your Discord bot easily.
Please note that this template is not supposed to be the best template, but a good template to start learning how
discord.py works and to make your own bot easily.
If you plan to use this template to make your own template or bot, you **have to**:
- Keep the credits, and a link to this repository in all the files that contains my code
- Keep the same license for unchanged code
See [the license file](https://github.com/kkrypt0nn/Python-Discord-Bot-Template/blob/master/LICENSE.md) for more
information, I reserve the right to take down any repository that does not meet these requirements.
## Support
Before requesting support, you should know that this template requires you to have at least a **basic knowledge** of
Python and the library is made for **advanced users**. Do not use this template if you don't know the
basics or some advanced topics such as OOP or async. [Here's](https://pythondiscord.com/pages/resources) a link for resources to learn python.
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.
All the updates of the template are available [here](UPDATES.md).
## Disclaimer
Slash commands can take some time to get registered globally, so if you want to test a command you should use
the `@app_commands.guilds()` decorator so that it gets registered instantly. Example:
```py
@commands.hybrid_command(
name="command",
description="Command description",
)
@app_commands.guilds(discord.Object(id=GUILD_ID)) # Place your guild ID here
```
When using the template you confirm that you have read the [license](LICENSE.md) and comprehend that I can take down
your repository if you do not meet these requirements.
## How to download it
This repository is now a template, on the top left you can simply click on "**Use this template**" to create a GitHub
repository based on this template.
Alternatively you can do the following:
- Clone/Download the repository
- To clone it and get the updates you can definitely use the command
`git clone`
- Create a Discord bot [here](https://discord.com/developers/applications)
- Get your bot token
- Invite your bot on servers using the following invite:
https://discord.com/oauth2/authorize?&client_id=YOUR_APPLICATION_ID_HERE&scope=bot+applications.commands&permissions=PERMISSIONS (
Replace `YOUR_APPLICATION_ID_HERE` with the application ID and replace `PERMISSIONS` with the required permissions
your bot needs that it can be get at the bottom of a this
page https://discord.com/developers/applications/YOUR_APPLICATION_ID_HERE/bot)
## How to set up
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.
Alternatively you can simply create a system environment variable with the same names and their respective value.
## How to start
### The _"usual"_ way
To start the bot you simply need to launch, either your terminal (Linux, Mac & Windows), or your Command Prompt (
Windows)
.
Before running the bot you will need to install all the requirements with this command:
```
python -m pip install -r requirements.txt
```
After that you can start it with
```
python bot.py
```
> **Note**: You may need to replace `python` with `py`, `python3`, `python3.11`, etc. depending on what Python versions you have installed on the machine.
### Docker
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:
```
docker compose up -d --build
```
> **Note**: `-d` will make the container run in detached mode, so in the background.
## Issues or Questions
If you have any issues or questions of how to code a specific command, you can:
- Join my Discord server [here](https://discord.gg/xj6y5ZaTMr)
- Post them [here](https://github.com/kkrypt0nn/Python-Discord-Bot-Template/issues)
Me or other people will take their time to answer and help you.
## Versioning
We use [SemVer](http://semver.org) for versioning. For the versions available, see
the [tags on this repository](https://github.com/kkrypt0nn/Python-Discord-Bot-Template/tags).
## Built With
- [Python 3.12.9](https://www.python.org/)
## License
This project is licensed under the Apache License 2.0 - see the [LICENSE.md](LICENSE.md) file for details

289
bot.py Normal file
View File

@@ -0,0 +1,289 @@
"""
Copyright © Krypton 2019-Present - https://github.com/kkrypt0nn (https://krypton.ninja)
Description:
🐍 A simple template to start to code your own and personalized Discord bot in Python
Version: 6.4.0
"""
import json
import logging
import os
import platform
import random
import sys
import aiosqlite
import discord
from discord.ext import commands, tasks
from discord.ext.commands import Context
from dotenv import load_dotenv
from database import DatabaseManager
load_dotenv()
"""
Setup bot intents (events restrictions)
For more information about intents, please go to the following websites:
https://discordpy.readthedocs.io/en/latest/intents.html
https://discordpy.readthedocs.io/en/latest/intents.html#privileged-intents
Default Intents:
intents.bans = True
intents.dm_messages = True
intents.dm_reactions = True
intents.dm_typing = True
intents.emojis = True
intents.emojis_and_stickers = True
intents.guild_messages = True
intents.guild_reactions = True
intents.guild_scheduled_events = True
intents.guild_typing = True
intents.guilds = True
intents.integrations = True
intents.invites = True
intents.messages = True # `message_content` is required to get the content of the messages
intents.reactions = True
intents.typing = True
intents.voice_states = True
intents.webhooks = True
Privileged Intents (Needs to be enabled on developer portal of Discord), please use them only if you need them:
intents.members = True
intents.message_content = True
intents.presences = True
"""
intents = discord.Intents.default()
"""
Uncomment this if you want to use prefix (normal) commands.
It is recommended to use slash commands and therefore not use prefix commands.
If you want to use prefix commands, make sure to also enable the intent below in the Discord developer portal.
"""
# intents.message_content = True
# Setup both of the loggers
class LoggingFormatter(logging.Formatter):
# Colors
black = "\x1b[30m"
red = "\x1b[31m"
green = "\x1b[32m"
yellow = "\x1b[33m"
blue = "\x1b[34m"
gray = "\x1b[38m"
# Styles
reset = "\x1b[0m"
bold = "\x1b[1m"
COLORS = {
logging.DEBUG: gray + bold,
logging.INFO: blue + bold,
logging.WARNING: yellow + bold,
logging.ERROR: red,
logging.CRITICAL: red + bold,
}
def format(self, record):
log_color = self.COLORS[record.levelno]
format = "(black){asctime}(reset) (levelcolor){levelname:<8}(reset) (green){name}(reset) {message}"
format = format.replace("(black)", self.black + self.bold)
format = format.replace("(reset)", self.reset)
format = format.replace("(levelcolor)", log_color)
format = format.replace("(green)", self.green + self.bold)
formatter = logging.Formatter(format, "%Y-%m-%d %H:%M:%S", style="{")
return formatter.format(record)
logger = logging.getLogger("discord_bot")
logger.setLevel(logging.INFO)
# Console handler
console_handler = logging.StreamHandler()
console_handler.setFormatter(LoggingFormatter())
# File handler
file_handler = logging.FileHandler(filename="discord.log", encoding="utf-8", mode="w")
file_handler_formatter = logging.Formatter(
"[{asctime}] [{levelname:<8}] {name}: {message}", "%Y-%m-%d %H:%M:%S", style="{"
)
file_handler.setFormatter(file_handler_formatter)
# Add the handlers
logger.addHandler(console_handler)
logger.addHandler(file_handler)
class DiscordBot(commands.Bot):
def __init__(self) -> None:
super().__init__(
command_prefix=commands.when_mentioned_or(os.getenv("PREFIX")),
intents=intents,
help_command=None,
)
"""
This creates custom bot variables so that we can access these variables in cogs more easily.
For example, The logger is available using the following code:
- self.logger # In this class
- bot.logger # In this file
- self.bot.logger # In cogs
"""
self.logger = logger
self.database = None
self.bot_prefix = os.getenv("PREFIX")
self.invite_link = os.getenv("INVITE_LINK")
async def init_db(self) -> None:
async with aiosqlite.connect(
f"{os.path.realpath(os.path.dirname(__file__))}/database/database.db"
) as db:
with open(
f"{os.path.realpath(os.path.dirname(__file__))}/database/schema.sql",
encoding = "utf-8"
) as file:
await db.executescript(file.read())
await db.commit()
async def load_cogs(self) -> None:
"""
The code in this function is executed whenever the bot will start.
"""
for file in os.listdir(f"{os.path.realpath(os.path.dirname(__file__))}/cogs"):
if file.endswith(".py"):
extension = file[:-3]
try:
await self.load_extension(f"cogs.{extension}")
self.logger.info(f"Loaded extension '{extension}'")
except Exception as e:
exception = f"{type(e).__name__}: {e}"
self.logger.error(
f"Failed to load extension {extension}\n{exception}"
)
@tasks.loop(minutes=1.0)
async def status_task(self) -> None:
"""
Setup the game status task of the bot.
"""
statuses = ["with you!", "with Krypton!", "with humans!"]
await self.change_presence(activity=discord.Game(random.choice(statuses)))
@status_task.before_loop
async def before_status_task(self) -> None:
"""
Before starting the status changing task, we make sure the bot is ready
"""
await self.wait_until_ready()
async def setup_hook(self) -> None:
"""
This will just be executed when the bot starts the first time.
"""
self.logger.info(f"Logged in as {self.user.name}")
self.logger.info(f"discord.py API version: {discord.__version__}")
self.logger.info(f"Python version: {platform.python_version()}")
self.logger.info(
f"Running on: {platform.system()} {platform.release()} ({os.name})"
)
self.logger.info("-------------------")
await self.init_db()
await self.load_cogs()
self.status_task.start()
self.database = DatabaseManager(
connection=await aiosqlite.connect(
f"{os.path.realpath(os.path.dirname(__file__))}/database/database.db"
)
)
async def on_message(self, message: discord.Message) -> None:
"""
The code in this event is executed every time someone sends a message, with or without the prefix
:param message: The message that was sent.
"""
if message.author == self.user or message.author.bot:
return
await self.process_commands(message)
async def on_command_completion(self, context: Context) -> None:
"""
The code in this event is executed every time a normal command has been *successfully* executed.
:param context: The context of the command that has been executed.
"""
full_command_name = context.command.qualified_name
split = full_command_name.split(" ")
executed_command = str(split[0])
if context.guild is not None:
self.logger.info(
f"Executed {executed_command} command in {context.guild.name} (ID: {context.guild.id}) by {context.author} (ID: {context.author.id})"
)
else:
self.logger.info(
f"Executed {executed_command} command by {context.author} (ID: {context.author.id}) in DMs"
)
async def on_command_error(self, context: Context, error) -> None:
"""
The code in this event is executed every time a normal valid command catches an error.
:param context: The context of the normal command that failed executing.
:param error: The error that has been faced.
"""
if isinstance(error, commands.CommandOnCooldown):
minutes, seconds = divmod(error.retry_after, 60)
hours, minutes = divmod(minutes, 60)
hours = hours % 24
embed = discord.Embed(
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 ''}.",
color=0xE02B2B,
)
await context.send(embed=embed)
elif isinstance(error, commands.NotOwner):
embed = discord.Embed(
description="You are not the owner of the bot!", color=0xE02B2B
)
await context.send(embed=embed)
if context.guild:
self.logger.warning(
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."
)
else:
self.logger.warning(
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."
)
elif isinstance(error, commands.MissingPermissions):
embed = discord.Embed(
description="You are missing the permission(s) `"
+ ", ".join(error.missing_permissions)
+ "` to execute this command!",
color=0xE02B2B,
)
await context.send(embed=embed)
elif isinstance(error, commands.BotMissingPermissions):
embed = discord.Embed(
description="I am missing the permission(s) `"
+ ", ".join(error.missing_permissions)
+ "` to fully perform this command!",
color=0xE02B2B,
)
await context.send(embed=embed)
elif isinstance(error, commands.MissingRequiredArgument):
embed = discord.Embed(
title="Error!",
# 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.
description=str(error).capitalize(),
color=0xE02B2B,
)
await context.send(embed=embed)
else:
raise error
bot = DiscordBot()
bot.run(os.getenv("TOKEN"))

163
cogs/fun.py Normal file
View File

@@ -0,0 +1,163 @@
"""
Copyright © Krypton 2019-Present - https://github.com/kkrypt0nn (https://krypton.ninja)
Description:
🐍 A simple template to start to code your own and personalized Discord bot in Python
Version: 6.4.0
"""
import random
import aiohttp
import discord
from discord.ext import commands
from discord.ext.commands import Context
class Choice(discord.ui.View):
def __init__(self) -> None:
super().__init__()
self.value = None
@discord.ui.button(label="Heads", style=discord.ButtonStyle.blurple)
async def confirm(
self, interaction: discord.Interaction, button: discord.ui.Button
) -> None:
self.value = "heads"
self.stop()
@discord.ui.button(label="Tails", style=discord.ButtonStyle.blurple)
async def cancel(
self, interaction: discord.Interaction, button: discord.ui.Button
) -> None:
self.value = "tails"
self.stop()
class RockPaperScissors(discord.ui.Select):
def __init__(self) -> None:
options = [
discord.SelectOption(
label="Scissors", description="You choose scissors.", emoji=""
),
discord.SelectOption(
label="Rock", description="You choose rock.", emoji="🪨"
),
discord.SelectOption(
label="Paper", description="You choose paper.", emoji="🧻"
),
]
super().__init__(
placeholder="Choose...",
min_values=1,
max_values=1,
options=options,
)
async def callback(self, interaction: discord.Interaction) -> None:
choices = {
"rock": 0,
"paper": 1,
"scissors": 2,
}
user_choice = self.values[0].lower()
user_choice_index = choices[user_choice]
bot_choice = random.choice(list(choices.keys()))
bot_choice_index = choices[bot_choice]
result_embed = discord.Embed(color=0xBEBEFE)
result_embed.set_author(
name=interaction.user.name, icon_url=interaction.user.display_avatar.url
)
winner = (3 + user_choice_index - bot_choice_index) % 3
if winner == 0:
result_embed.description = f"**That's a draw!**\nYou've chosen {user_choice} and I've chosen {bot_choice}."
result_embed.colour = 0xF59E42
elif winner == 1:
result_embed.description = f"**You won!**\nYou've chosen {user_choice} and I've chosen {bot_choice}."
result_embed.colour = 0x57F287
else:
result_embed.description = f"**You lost!**\nYou've chosen {user_choice} and I've chosen {bot_choice}."
result_embed.colour = 0xE02B2B
await interaction.response.edit_message(
embed=result_embed, content=None, view=None
)
class RockPaperScissorsView(discord.ui.View):
def __init__(self) -> None:
super().__init__()
self.add_item(RockPaperScissors())
class Fun(commands.Cog, name="fun"):
def __init__(self, bot) -> None:
self.bot = bot
@commands.hybrid_command(name="randomfact", description="Get a random fact.")
async def randomfact(self, context: Context) -> None:
"""
Get a random fact.
:param context: The hybrid command context.
"""
# 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
async with aiohttp.ClientSession() as session:
async with session.get(
"https://uselessfacts.jsph.pl/random.json?language=en"
) as request:
if request.status == 200:
data = await request.json()
embed = discord.Embed(description=data["text"], color=0xD75BF4)
else:
embed = discord.Embed(
title="Error!",
description="There is something wrong with the API, please try again later",
color=0xE02B2B,
)
await context.send(embed=embed)
@commands.hybrid_command(
name="coinflip", description="Make a coin flip, but give your bet before."
)
async def coinflip(self, context: Context) -> None:
"""
Make a coin flip, but give your bet before.
:param context: The hybrid command context.
"""
buttons = Choice()
embed = discord.Embed(description="What is your bet?", color=0xBEBEFE)
message = await context.send(embed=embed, view=buttons)
await buttons.wait() # We wait for the user to click a button.
result = random.choice(["heads", "tails"])
if buttons.value == result:
embed = discord.Embed(
description=f"Correct! You guessed `{buttons.value}` and I flipped the coin to `{result}`.",
color=0xBEBEFE,
)
else:
embed = discord.Embed(
description=f"Woops! You guessed `{buttons.value}` and I flipped the coin to `{result}`, better luck next time!",
color=0xE02B2B,
)
await message.edit(embed=embed, view=None, content=None)
@commands.hybrid_command(
name="rps", description="Play the rock paper scissors game against the bot."
)
async def rock_paper_scissors(self, context: Context) -> None:
"""
Play the rock paper scissors game against the bot.
:param context: The hybrid command context.
"""
view = RockPaperScissorsView()
await context.send("Please make your choice", view=view)
async def setup(bot) -> None:
await bot.add_cog(Fun(bot))

327
cogs/general.py Normal file
View File

@@ -0,0 +1,327 @@
"""
Copyright © Krypton 2019-Present - https://github.com/kkrypt0nn (https://krypton.ninja)
Description:
🐍 A simple template to start to code your own and personalized Discord bot in Python
Version: 6.4.0
"""
import platform
import random
import aiohttp
import discord
from discord import app_commands
from discord.ext import commands
from discord.ext.commands import Context
class FeedbackForm(discord.ui.Modal, title="Feeedback"):
feedback = discord.ui.TextInput(
label="What do you think about this bot?",
style=discord.TextStyle.long,
placeholder="Type your answer here...",
required=True,
max_length=256,
)
async def on_submit(self, interaction: discord.Interaction):
self.interaction = interaction
self.answer = str(self.feedback)
self.stop()
class General(commands.Cog, name="general"):
def __init__(self, bot) -> None:
self.bot = bot
self.context_menu_user = app_commands.ContextMenu(
name="Grab ID", callback=self.grab_id
)
self.bot.tree.add_command(self.context_menu_user)
self.context_menu_message = app_commands.ContextMenu(
name="Remove spoilers", callback=self.remove_spoilers
)
self.bot.tree.add_command(self.context_menu_message)
# Message context menu command
async def remove_spoilers(
self, interaction: discord.Interaction, message: discord.Message
) -> None:
"""
Removes the spoilers from the message. This command requires the MESSAGE_CONTENT intent to work properly.
:param interaction: The application command interaction.
:param message: The message that is being interacted with.
"""
spoiler_attachment = None
for attachment in message.attachments:
if attachment.is_spoiler():
spoiler_attachment = attachment
break
embed = discord.Embed(
title="Message without spoilers",
description=message.content.replace("||", ""),
color=0xBEBEFE,
)
if spoiler_attachment is not None:
embed.set_image(url=attachment.url)
await interaction.response.send_message(embed=embed, ephemeral=True)
# User context menu command
async def grab_id(
self, interaction: discord.Interaction, user: discord.User
) -> None:
"""
Grabs the ID of the user.
:param interaction: The application command interaction.
:param user: The user that is being interacted with.
"""
embed = discord.Embed(
description=f"The ID of {user.mention} is `{user.id}`.",
color=0xBEBEFE,
)
await interaction.response.send_message(embed=embed, ephemeral=True)
@commands.hybrid_command(
name="help", description="List all commands the bot has loaded."
)
async def help(self, context: Context) -> None:
embed = discord.Embed(
title="Help", description="List of available commands:", color=0xBEBEFE
)
for i in self.bot.cogs:
if i == "owner" and not (await self.bot.is_owner(context.author)):
continue
cog = self.bot.get_cog(i.lower())
commands = cog.get_commands()
data = []
for command in commands:
description = command.description.partition("\n")[0]
data.append(f"{command.name} - {description}")
help_text = "\n".join(data)
embed.add_field(
name=i.capitalize(), value=f"```{help_text}```", inline=False
)
await context.send(embed=embed)
@commands.hybrid_command(
name="botinfo",
description="Get some useful (or not) information about the bot.",
)
async def botinfo(self, context: Context) -> None:
"""
Get some useful (or not) information about the bot.
:param context: The hybrid command context.
"""
embed = discord.Embed(
description="Used [Krypton's](https://krypton.ninja) template",
color=0xBEBEFE,
)
embed.set_author(name="Bot Information")
embed.add_field(name="Owner:", value="Krypton#7331", inline=True)
embed.add_field(
name="Python Version:", value=f"{platform.python_version()}", inline=True
)
embed.add_field(
name="Prefix:",
value=f"/ (Slash Commands) or {self.bot.bot_prefix} for normal commands",
inline=False,
)
embed.set_footer(text=f"Requested by {context.author}")
await context.send(embed=embed)
@commands.hybrid_command(
name="serverinfo",
description="Get some useful (or not) information about the server.",
)
async def serverinfo(self, context: Context) -> None:
"""
Get some useful (or not) information about the server.
:param context: The hybrid command context.
"""
roles = [role.name for role in context.guild.roles]
num_roles = len(roles)
if num_roles > 50:
roles = roles[:50]
roles.append(f">>>> Displaying [50/{num_roles}] Roles")
roles = ", ".join(roles)
embed = discord.Embed(
title="**Server Name:**", description=f"{context.guild}", color=0xBEBEFE
)
if context.guild.icon is not None:
embed.set_thumbnail(url=context.guild.icon.url)
embed.add_field(name="Server ID", value=context.guild.id)
embed.add_field(name="Member Count", value=context.guild.member_count)
embed.add_field(
name="Text/Voice Channels", value=f"{len(context.guild.channels)}"
)
embed.add_field(name=f"Roles ({len(context.guild.roles)})", value=roles)
embed.set_footer(text=f"Created at: {context.guild.created_at}")
await context.send(embed=embed)
@commands.hybrid_command(
name="ping",
description="Check if the bot is alive.",
)
async def ping(self, context: Context) -> None:
"""
Check if the bot is alive.
:param context: The hybrid command context.
"""
embed = discord.Embed(
title="🏓 Pong!",
description=f"The bot latency is {round(self.bot.latency * 1000)}ms.",
color=0xBEBEFE,
)
await context.send(embed=embed)
@commands.hybrid_command(
name="invite",
description="Get the invite link of the bot to be able to invite it.",
)
async def invite(self, context: Context) -> None:
"""
Get the invite link of the bot to be able to invite it.
:param context: The hybrid command context.
"""
embed = discord.Embed(
description=f"Invite me by clicking [here]({self.bot.invite_link}).",
color=0xD75BF4,
)
try:
await context.author.send(embed=embed)
await context.send("I sent you a private message!")
except discord.Forbidden:
await context.send(embed=embed)
@commands.hybrid_command(
name="server",
description="Get the invite link of the discord server of the bot for some support.",
)
async def server(self, context: Context) -> None:
"""
Get the invite link of the discord server of the bot for some support.
:param context: The hybrid command context.
"""
embed = discord.Embed(
description=f"Join the support server for the bot by clicking [here](https://discord.gg/mTBrXyWxAF).",
color=0xD75BF4,
)
try:
await context.author.send(embed=embed)
await context.send("I sent you a private message!")
except discord.Forbidden:
await context.send(embed=embed)
@commands.hybrid_command(
name="8ball",
description="Ask any question to the bot.",
)
@app_commands.describe(question="The question you want to ask.")
async def eight_ball(self, context: Context, *, question: str) -> None:
"""
Ask any question to the bot.
:param context: The hybrid command context.
:param question: The question that should be asked by the user.
"""
answers = [
"It is certain.",
"It is decidedly so.",
"You may rely on it.",
"Without a doubt.",
"Yes - definitely.",
"As I see, yes.",
"Most likely.",
"Outlook good.",
"Yes.",
"Signs point to yes.",
"Reply hazy, try again.",
"Ask again later.",
"Better not tell you now.",
"Cannot predict now.",
"Concentrate and ask again later.",
"Don't count on it.",
"My reply is no.",
"My sources say no.",
"Outlook not so good.",
"Very doubtful.",
]
embed = discord.Embed(
title="**My Answer:**",
description=f"{random.choice(answers)}",
color=0xBEBEFE,
)
embed.set_footer(text=f"The question was: {question}")
await context.send(embed=embed)
@commands.hybrid_command(
name="bitcoin",
description="Get the current price of bitcoin.",
)
async def bitcoin(self, context: Context) -> None:
"""
Get the current price of bitcoin.
:param context: The hybrid command context.
"""
# 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
async with aiohttp.ClientSession() as session:
async with session.get(
"https://api.coindesk.com/v1/bpi/currentprice/BTC.json"
) as request:
if request.status == 200:
data = await request.json()
embed = discord.Embed(
title="Bitcoin price",
description=f"The current price is {data['bpi']['USD']['rate']} :dollar:",
color=0xBEBEFE,
)
else:
embed = discord.Embed(
title="Error!",
description="There is something wrong with the API, please try again later",
color=0xE02B2B,
)
await context.send(embed=embed)
@app_commands.command(
name="feedback", description="Submit a feedback for the owners of the bot"
)
async def feedback(self, interaction: discord.Interaction) -> None:
"""
Submit a feedback for the owners of the bot.
:param context: The hybrid command context.
"""
feedback_form = FeedbackForm()
await interaction.response.send_modal(feedback_form)
await feedback_form.wait()
interaction = feedback_form.interaction
await interaction.response.send_message(
embed=discord.Embed(
description="Thank you for your feedback, the owners have been notified about it.",
color=0xBEBEFE,
)
)
app_owner = (await self.bot.application_info()).owner
await app_owner.send(
embed=discord.Embed(
title="New Feedback",
description=f"{interaction.user} (<@{interaction.user.id}>) has submitted a new feedback:\n```\n{feedback_form.answer}\n```",
color=0xBEBEFE,
)
)
async def setup(bot) -> None:
await bot.add_cog(General(bot))

375
cogs/moderation.py Normal file
View File

@@ -0,0 +1,375 @@
"""
Copyright © Krypton 2019-Present - https://github.com/kkrypt0nn (https://krypton.ninja)
Description:
🐍 A simple template to start to code your own and personalized Discord bot in Python
Version: 6.4.0
"""
import os
from datetime import datetime
import discord
from discord import app_commands
from discord.ext import commands
from discord.ext.commands import Context
class Moderation(commands.Cog, name="moderation"):
def __init__(self, bot) -> None:
self.bot = bot
@commands.hybrid_command(
name="kick",
description="Kick a user out of the server.",
)
@commands.has_permissions(kick_members=True)
@commands.bot_has_permissions(kick_members=True)
@app_commands.describe(
user="The user that should be kicked.",
reason="The reason why the user should be kicked.",
)
async def kick(
self, context: Context, user: discord.User, *, reason: str = "Not specified"
) -> None:
"""
Kick a user out of the server.
:param context: The hybrid command context.
:param user: The user that should be kicked from the server.
:param reason: The reason for the kick. Default is "Not specified".
"""
member = context.guild.get_member(user.id) or await context.guild.fetch_member(
user.id
)
if member.guild_permissions.administrator:
embed = discord.Embed(
description="User has administrator permissions.", color=0xE02B2B
)
await context.send(embed=embed)
else:
try:
embed = discord.Embed(
description=f"**{member}** was kicked by **{context.author}**!",
color=0xBEBEFE,
)
embed.add_field(name="Reason:", value=reason)
await context.send(embed=embed)
try:
await member.send(
f"You were kicked by **{context.author}** from **{context.guild.name}**!\nReason: {reason}"
)
except:
# Couldn't send a message in the private messages of the user
pass
await member.kick(reason=reason)
except:
embed = discord.Embed(
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.",
color=0xE02B2B,
)
await context.send(embed=embed)
@commands.hybrid_command(
name="nick",
description="Change the nickname of a user on a server.",
)
@commands.has_permissions(manage_nicknames=True)
@commands.bot_has_permissions(manage_nicknames=True)
@app_commands.describe(
user="The user that should have a new nickname.",
nickname="The new nickname that should be set.",
)
async def nick(
self, context: Context, user: discord.User, *, nickname: str = None
) -> None:
"""
Change the nickname of a user on a server.
:param context: The hybrid command context.
:param user: The user that should have its nickname changed.
:param nickname: The new nickname of the user. Default is None, which will reset the nickname.
"""
member = context.guild.get_member(user.id) or await context.guild.fetch_member(
user.id
)
try:
await member.edit(nick=nickname)
embed = discord.Embed(
description=f"**{member}'s** new nickname is **{nickname}**!",
color=0xBEBEFE,
)
await context.send(embed=embed)
except:
embed = discord.Embed(
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.",
color=0xE02B2B,
)
await context.send(embed=embed)
@commands.hybrid_command(
name="ban",
description="Bans a user from the server.",
)
@commands.has_permissions(ban_members=True)
@commands.bot_has_permissions(ban_members=True)
@app_commands.describe(
user="The user that should be banned.",
reason="The reason why the user should be banned.",
)
async def ban(
self, context: Context, user: discord.User, *, reason: str = "Not specified"
) -> None:
"""
Bans a user from the server.
:param context: The hybrid command context.
:param user: The user that should be banned from the server.
:param reason: The reason for the ban. Default is "Not specified".
"""
member = context.guild.get_member(user.id) or await context.guild.fetch_member(
user.id
)
try:
if member.guild_permissions.administrator:
embed = discord.Embed(
description="User has administrator permissions.", color=0xE02B2B
)
await context.send(embed=embed)
else:
embed = discord.Embed(
description=f"**{member}** was banned by **{context.author}**!",
color=0xBEBEFE,
)
embed.add_field(name="Reason:", value=reason)
await context.send(embed=embed)
try:
await member.send(
f"You were banned by **{context.author}** from **{context.guild.name}**!\nReason: {reason}"
)
except:
# Couldn't send a message in the private messages of the user
pass
await member.ban(reason=reason)
except:
embed = discord.Embed(
title="Error!",
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.",
color=0xE02B2B,
)
await context.send(embed=embed)
@commands.hybrid_group(
name="warning",
description="Manage warnings of a user on a server.",
)
@commands.has_permissions(manage_messages=True)
async def warning(self, context: Context) -> None:
"""
Manage warnings of a user on a server.
:param context: The hybrid command context.
"""
if context.invoked_subcommand is None:
embed = discord.Embed(
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.",
color=0xE02B2B,
)
await context.send(embed=embed)
@warning.command(
name="add",
description="Adds a warning to a user in the server.",
)
@commands.has_permissions(manage_messages=True)
@app_commands.describe(
user="The user that should be warned.",
reason="The reason why the user should be warned.",
)
async def warning_add(
self, context: Context, user: discord.User, *, reason: str = "Not specified"
) -> None:
"""
Warns a user in his private messages.
:param context: The hybrid command context.
:param user: The user that should be warned.
:param reason: The reason for the warn. Default is "Not specified".
"""
member = context.guild.get_member(user.id) or await context.guild.fetch_member(
user.id
)
total = await self.bot.database.add_warn(
user.id, context.guild.id, context.author.id, reason
)
embed = discord.Embed(
description=f"**{member}** was warned by **{context.author}**!\nTotal warns for this user: {total}",
color=0xBEBEFE,
)
embed.add_field(name="Reason:", value=reason)
await context.send(embed=embed)
try:
await member.send(
f"You were warned by **{context.author}** in **{context.guild.name}**!\nReason: {reason}"
)
except:
# Couldn't send a message in the private messages of the user
await context.send(
f"{member.mention}, you were warned by **{context.author}**!\nReason: {reason}"
)
@warning.command(
name="remove",
description="Removes a warning from a user in the server.",
)
@commands.has_permissions(manage_messages=True)
@app_commands.describe(
user="The user that should get their warning removed.",
warn_id="The ID of the warning that should be removed.",
)
async def warning_remove(
self, context: Context, user: discord.User, warn_id: int
) -> None:
"""
Warns a user in his private messages.
:param context: The hybrid command context.
:param user: The user that should get their warning removed.
:param warn_id: The ID of the warning that should be removed.
"""
member = context.guild.get_member(user.id) or await context.guild.fetch_member(
user.id
)
total = await self.bot.database.remove_warn(warn_id, user.id, context.guild.id)
embed = discord.Embed(
description=f"I've removed the warning **#{warn_id}** from **{member}**!\nTotal warns for this user: {total}",
color=0xBEBEFE,
)
await context.send(embed=embed)
@warning.command(
name="list",
description="Shows the warnings of a user in the server.",
)
@commands.has_guild_permissions(manage_messages=True)
@app_commands.describe(user="The user you want to get the warnings of.")
async def warning_list(self, context: Context, user: discord.User) -> None:
"""
Shows the warnings of a user in the server.
:param context: The hybrid command context.
:param user: The user you want to get the warnings of.
"""
warnings_list = await self.bot.database.get_warnings(user.id, context.guild.id)
embed = discord.Embed(title=f"Warnings of {user}", color=0xBEBEFE)
description = ""
if len(warnings_list) == 0:
description = "This user has no warnings."
else:
for warning in warnings_list:
description += f"• Warned by <@{warning[2]}>: **{warning[3]}** (<t:{warning[4]}>) - Warn ID #{warning[5]}\n"
embed.description = description
await context.send(embed=embed)
@commands.hybrid_command(
name="purge",
description="Delete a number of messages.",
)
@commands.has_guild_permissions(manage_messages=True)
@commands.bot_has_permissions(manage_messages=True)
@app_commands.describe(amount="The amount of messages that should be deleted.")
async def purge(self, context: Context, amount: int) -> None:
"""
Delete a number of messages.
:param context: The hybrid command context.
:param amount: The number of messages that should be deleted.
"""
await context.send(
"Deleting messages..."
) # Bit of a hacky way to make sure the bot responds to the interaction and doens't get a "Unknown Interaction" response
purged_messages = await context.channel.purge(limit=amount + 1)
embed = discord.Embed(
description=f"**{context.author}** cleared **{len(purged_messages)-1}** messages!",
color=0xBEBEFE,
)
await context.channel.send(embed=embed)
@commands.hybrid_command(
name="hackban",
description="Bans a user without the user having to be in the server.",
)
@commands.has_permissions(ban_members=True)
@commands.bot_has_permissions(ban_members=True)
@app_commands.describe(
user_id="The user ID that should be banned.",
reason="The reason why the user should be banned.",
)
async def hackban(
self, context: Context, user_id: str, *, reason: str = "Not specified"
) -> None:
"""
Bans a user without the user having to be in the server.
:param context: The hybrid command context.
:param user_id: The ID of the user that should be banned.
:param reason: The reason for the ban. Default is "Not specified".
"""
try:
await self.bot.http.ban(user_id, context.guild.id, reason=reason)
user = self.bot.get_user(int(user_id)) or await self.bot.fetch_user(
int(user_id)
)
embed = discord.Embed(
description=f"**{user}** (ID: {user_id}) was banned by **{context.author}**!",
color=0xBEBEFE,
)
embed.add_field(name="Reason:", value=reason)
await context.send(embed=embed)
except Exception:
embed = discord.Embed(
description="An error occurred while trying to ban the user. Make sure ID is an existing ID that belongs to a user.",
color=0xE02B2B,
)
await context.send(embed=embed)
@commands.hybrid_command(
name="archive",
description="Archives in a text file the last messages with a chosen limit of messages.",
)
@commands.has_permissions(manage_messages=True)
@app_commands.describe(
limit="The limit of messages that should be archived.",
)
async def archive(self, context: Context, limit: int = 10) -> None:
"""
Archives in a text file the last messages with a chosen limit of messages. This command requires the MESSAGE_CONTENT intent to work properly.
:param limit: The limit of messages that should be archived. Default is 10.
"""
log_file = f"{context.channel.id}.log"
with open(log_file, "w", encoding="UTF-8") as f:
f.write(
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'
)
async for message in context.channel.history(
limit=limit, before=context.message
):
attachments = []
for attachment in message.attachments:
attachments.append(attachment.url)
attachments_text = (
f"[Attached File{'s' if len(attachments) >= 2 else ''}: {', '.join(attachments)}]"
if len(attachments) >= 1
else ""
)
f.write(
f"{message.created_at.strftime('%d.%m.%Y %H:%M:%S')} {message.author} {message.id}: {message.clean_content} {attachments_text}\n"
)
f = discord.File(log_file)
await context.send(file=f)
os.remove(log_file)
async def setup(bot) -> None:
await bot.add_cog(Moderation(bot))

220
cogs/owner.py Normal file
View File

@@ -0,0 +1,220 @@
"""
Copyright © Krypton 2019-Present - https://github.com/kkrypt0nn (https://krypton.ninja)
Description:
🐍 A simple template to start to code your own and personalized Discord bot in Python
Version: 6.4.0
"""
import discord
from discord import app_commands
from discord.ext import commands
from discord.ext.commands import Context
class Owner(commands.Cog, name="owner"):
def __init__(self, bot) -> None:
self.bot = bot
@commands.command(
name="sync",
description="Synchonizes the slash commands.",
)
@app_commands.describe(scope="The scope of the sync. Can be `global` or `guild`")
@commands.is_owner()
async def sync(self, context: Context, scope: str) -> None:
"""
Synchonizes the slash commands.
:param context: The command context.
:param scope: The scope of the sync. Can be `global` or `guild`.
"""
if scope == "global":
await context.bot.tree.sync()
embed = discord.Embed(
description="Slash commands have been globally synchronized.",
color=0xBEBEFE,
)
await context.send(embed=embed)
return
elif scope == "guild":
context.bot.tree.copy_global_to(guild=context.guild)
await context.bot.tree.sync(guild=context.guild)
embed = discord.Embed(
description="Slash commands have been synchronized in this guild.",
color=0xBEBEFE,
)
await context.send(embed=embed)
return
embed = discord.Embed(
description="The scope must be `global` or `guild`.", color=0xE02B2B
)
await context.send(embed=embed)
@commands.command(
name="unsync",
description="Unsynchonizes the slash commands.",
)
@app_commands.describe(
scope="The scope of the sync. Can be `global`, `current_guild` or `guild`"
)
@commands.is_owner()
async def unsync(self, context: Context, scope: str) -> None:
"""
Unsynchonizes the slash commands.
:param context: The command context.
:param scope: The scope of the sync. Can be `global`, `current_guild` or `guild`.
"""
if scope == "global":
context.bot.tree.clear_commands(guild=None)
await context.bot.tree.sync()
embed = discord.Embed(
description="Slash commands have been globally unsynchronized.",
color=0xBEBEFE,
)
await context.send(embed=embed)
return
elif scope == "guild":
context.bot.tree.clear_commands(guild=context.guild)
await context.bot.tree.sync(guild=context.guild)
embed = discord.Embed(
description="Slash commands have been unsynchronized in this guild.",
color=0xBEBEFE,
)
await context.send(embed=embed)
return
embed = discord.Embed(
description="The scope must be `global` or `guild`.", color=0xE02B2B
)
await context.send(embed=embed)
@commands.hybrid_command(
name="load",
description="Load a cog",
)
@app_commands.describe(cog="The name of the cog to load")
@commands.is_owner()
async def load(self, context: Context, cog: str) -> None:
"""
The bot will load the given cog.
:param context: The hybrid command context.
:param cog: The name of the cog to load.
"""
try:
await self.bot.load_extension(f"cogs.{cog}")
except Exception:
embed = discord.Embed(
description=f"Could not load the `{cog}` cog.", color=0xE02B2B
)
await context.send(embed=embed)
return
embed = discord.Embed(
description=f"Successfully loaded the `{cog}` cog.", color=0xBEBEFE
)
await context.send(embed=embed)
@commands.hybrid_command(
name="unload",
description="Unloads a cog.",
)
@app_commands.describe(cog="The name of the cog to unload")
@commands.is_owner()
async def unload(self, context: Context, cog: str) -> None:
"""
The bot will unload the given cog.
:param context: The hybrid command context.
:param cog: The name of the cog to unload.
"""
try:
await self.bot.unload_extension(f"cogs.{cog}")
except Exception:
embed = discord.Embed(
description=f"Could not unload the `{cog}` cog.", color=0xE02B2B
)
await context.send(embed=embed)
return
embed = discord.Embed(
description=f"Successfully unloaded the `{cog}` cog.", color=0xBEBEFE
)
await context.send(embed=embed)
@commands.hybrid_command(
name="reload",
description="Reloads a cog.",
)
@app_commands.describe(cog="The name of the cog to reload")
@commands.is_owner()
async def reload(self, context: Context, cog: str) -> None:
"""
The bot will reload the given cog.
:param context: The hybrid command context.
:param cog: The name of the cog to reload.
"""
try:
await self.bot.reload_extension(f"cogs.{cog}")
except Exception:
embed = discord.Embed(
description=f"Could not reload the `{cog}` cog.", color=0xE02B2B
)
await context.send(embed=embed)
return
embed = discord.Embed(
description=f"Successfully reloaded the `{cog}` cog.", color=0xBEBEFE
)
await context.send(embed=embed)
@commands.hybrid_command(
name="shutdown",
description="Make the bot shutdown.",
)
@commands.is_owner()
async def shutdown(self, context: Context) -> None:
"""
Shuts down the bot.
:param context: The hybrid command context.
"""
embed = discord.Embed(description="Shutting down. Bye! :wave:", color=0xBEBEFE)
await context.send(embed=embed)
await self.bot.close()
@commands.hybrid_command(
name="say",
description="The bot will say anything you want.",
)
@app_commands.describe(message="The message that should be repeated by the bot")
@commands.is_owner()
async def say(self, context: Context, *, message: str) -> None:
"""
The bot will say anything you want.
:param context: The hybrid command context.
:param message: The message that should be repeated by the bot.
"""
await context.send(message)
@commands.hybrid_command(
name="embed",
description="The bot will say anything you want, but within embeds.",
)
@app_commands.describe(message="The message that should be repeated by the bot")
@commands.is_owner()
async def embed(self, context: Context, *, message: str) -> None:
"""
The bot will say anything you want, but using embeds.
:param context: The hybrid command context.
:param message: The message that should be repeated by the bot.
"""
embed = discord.Embed(description=message, color=0xBEBEFE)
await context.send(embed=embed)
async def setup(bot) -> None:
await bot.add_cog(Owner(bot))

38
cogs/template.py Normal file
View File

@@ -0,0 +1,38 @@
"""
Copyright © Krypton 2019-Present - https://github.com/kkrypt0nn (https://krypton.ninja)
Description:
🐍 A simple template to start to code your own and personalized Discord bot in Python
Version: 6.4.0
"""
from discord.ext import commands
from discord.ext.commands import Context
# Here we name the cog and create a new class for the cog.
class Template(commands.Cog, name="template"):
def __init__(self, bot) -> None:
self.bot = bot
# Here you can just add your own commands, you'll always need to provide "self" as first parameter.
@commands.hybrid_command(
name="testcommand",
description="This is a testing command that does nothing.",
)
async def testcommand(self, context: Context) -> None:
"""
This is a testing command that does nothing.
:param context: The application command context.
"""
# Do your stuff here
# Don't forget to remove "pass", I added this just because there's no content in the method.
pass
# And then we finally add the cog to the bot so that it can load, unload, reload and use it's content.
async def setup(bot) -> None:
await bot.add_cog(Template(bot))

96
database/__init__.py Normal file
View File

@@ -0,0 +1,96 @@
"""
Copyright © Krypton 2019-Present - https://github.com/kkrypt0nn (https://krypton.ninja)
Description:
🐍 A simple template to start to code your own and personalized Discord bot in Python
Version: 6.4.0
"""
import aiosqlite
class DatabaseManager:
def __init__(self, *, connection: aiosqlite.Connection) -> None:
self.connection = connection
async def add_warn(
self, user_id: int, server_id: int, moderator_id: int, reason: str
) -> int:
"""
This function will add a warn to the database.
:param user_id: The ID of the user that should be warned.
:param reason: The reason why the user should be warned.
"""
rows = await self.connection.execute(
"SELECT id FROM warns WHERE user_id=? AND server_id=? ORDER BY id DESC LIMIT 1",
(
user_id,
server_id,
),
)
async with rows as cursor:
result = await cursor.fetchone()
warn_id = result[0] + 1 if result is not None else 1
await self.connection.execute(
"INSERT INTO warns(id, user_id, server_id, moderator_id, reason) VALUES (?, ?, ?, ?, ?)",
(
warn_id,
user_id,
server_id,
moderator_id,
reason,
),
)
await self.connection.commit()
return warn_id
async def remove_warn(self, warn_id: int, user_id: int, server_id: int) -> int:
"""
This function will remove a warn from the database.
:param warn_id: The ID of the warn.
:param user_id: The ID of the user that was warned.
:param server_id: The ID of the server where the user has been warned
"""
await self.connection.execute(
"DELETE FROM warns WHERE id=? AND user_id=? AND server_id=?",
(
warn_id,
user_id,
server_id,
),
)
await self.connection.commit()
rows = await self.connection.execute(
"SELECT COUNT(*) FROM warns WHERE user_id=? AND server_id=?",
(
user_id,
server_id,
),
)
async with rows as cursor:
result = await cursor.fetchone()
return result[0] if result is not None else 0
async def get_warnings(self, user_id: int, server_id: int) -> list:
"""
This function will get all the warnings of a user.
:param user_id: The ID of the user that should be checked.
:param server_id: The ID of the server that should be checked.
:return: A list of all the warnings of the user.
"""
rows = await self.connection.execute(
"SELECT user_id, server_id, moderator_id, reason, strftime('%s', created_at), id FROM warns WHERE user_id=? AND server_id=?",
(
user_id,
server_id,
),
)
async with rows as cursor:
result = await cursor.fetchall()
result_list = []
for row in result:
result_list.append(row)
return result_list

8
database/schema.sql Normal file
View File

@@ -0,0 +1,8 @@
CREATE TABLE IF NOT EXISTS `warns` (
`id` int(11) NOT NULL,
`user_id` varchar(20) NOT NULL,
`server_id` varchar(20) NOT NULL,
`moderator_id` varchar(20) NOT NULL,
`reason` varchar(255) NOT NULL,
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP
);

15
docker-compose.yml Normal file
View File

@@ -0,0 +1,15 @@
services:
discord-bot:
build: .
image: python-discord-bot-template
env_file:
- .env
volumes:
- ./database:/bot/database
- ./discord.log:/bot/discord.log
# Alternatively you can set the environment variables as such:
# /!\ The token shouldn't be written here, as this file is not ignored from Git /!\
# environment:
# - PREFIX=YOUR_BOT_PREFIX_HERE
# - INVITE_LINK=YOUR_BOT_INVITE_LINK_HERE

4
requirements.txt Normal file
View File

@@ -0,0 +1,4 @@
aiohttp
aiosqlite
discord.py==2.6.3
python-dotenv