├── utils ├── Errors.py ├── DBClient.py ├── ServerLogger.py ├── Checks.py ├── CachedDB.py ├── ErrorLogger.py └── CONSTANTS.py ├── requirements.txt ├── config.example.json ├── .env.example ├── Dockerfile ├── ui ├── starboard.py ├── recreate.py ├── translate.py ├── code.py ├── ticket.py ├── farm.py ├── papertrading.py └── setup.py ├── README.md ├── cogs ├── code.py ├── giveaway.py ├── api.py ├── reactionroles.py ├── github.py ├── utility.py ├── stats.py ├── level.py ├── starboard.py ├── music.py ├── economy.py ├── ticket.py └── server.py ├── .gitignore ├── main.py └── bot.py /utils/Errors.py: -------------------------------------------------------------------------------- 1 | from discord.ext.commands import CommandError 2 | 3 | class CommandDisabled(CommandError): 4 | pass 5 | 6 | class UserBlacklisted(CommandError): 7 | pass 8 | -------------------------------------------------------------------------------- /utils/DBClient.py: -------------------------------------------------------------------------------- 1 | # This project is licensed under the terms of the GPL v3.0 license. Copyright 2024 Cyteon 2 | 3 | import pymongo 4 | import os 5 | 6 | client = pymongo.MongoClient(os.getenv("MONGODB_URL")) 7 | db = client.potatobot 8 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp 2 | asteval 3 | asyncpraw 4 | better_profanity 5 | cryptography 6 | deep_translator 7 | discord.py 8 | easy_pil 9 | fastapi 10 | groq 11 | lavalink 12 | pickleDB 13 | pymongo 14 | python-dotenv 15 | redis 16 | uvicorn 17 | -------------------------------------------------------------------------------- /config.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "prefix": ",", 3 | "invite_link": "https://discord.com/oauth2/authorize?client_id=1226487228914602005&scope=bot&permissions=8", 4 | "command_error_webhook": "", 5 | "error_webhooks": "", 6 | "join_leave_webhook": "", 7 | "bug_channel": "1244584577989873684", 8 | "origins": ["http://localhost", "http://localhost:3000"], 9 | "ssl_keyfile": "./ssl/example.com.key", 10 | "ssl_certfile": "./ssl/example.com.pem", 11 | "use_ssl": false, 12 | "port": 80, 13 | "fully_ignore": [] 14 | } 15 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | OWNER_ID= 2 | TOKEN= 3 | 4 | GROQ_API_KEY_COUNT=1 5 | GROQ_API_KEY_1= 6 | 7 | HF_API_KEY= 8 | 9 | REDDIT_CLIENT_ID= 10 | REDDIT_CLIENT_SECRET= 11 | 12 | FUSION_API_KEY= 13 | FUSION_SECRET_KEY= 14 | TENOR_API_KEY= 15 | HASHING_SECRET="changeme" # change this to a random string 16 | ALPHA_VANTAGE_API_KEY= 17 | 18 | LAVALINK_HOST="0.0.0.0" 19 | LAVALINK_PORT=1234 20 | LAVALINK_PASSWORD="potatoes" 21 | LAVALINK_REGION="???" 22 | LAVALINK_NAME="default-node" 23 | 24 | REDIS_URL= 25 | MONGODB_URL= 26 | MONGODB_BACKUP_URL= 27 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use an official Python runtime as a parent image 2 | FROM python:3.9-slim-buster 3 | 4 | # Set the working directory in the container to /app 5 | WORKDIR /app 6 | 7 | # Add the current directory contents into the container at /app 8 | ADD . /app 9 | 10 | # Install any needed packages specified in requirements.txt 11 | RUN pip install --no-cache-dir -r requirements.txt 12 | 13 | # Make port 443 available to the world outside this container 14 | EXPOSE 443 15 | 16 | # Run app.py when the container launches 17 | CMD ["python", "main.py"] 18 | -------------------------------------------------------------------------------- /ui/starboard.py: -------------------------------------------------------------------------------- 1 | import discord 2 | 3 | class JumpToMessageButton(discord.ui.Button): 4 | def __init__(self, message: discord.message) -> None: 5 | super().__init__( 6 | style=discord.ButtonStyle.link, 7 | label="Jump to message", 8 | url=f"https://discord.com/channels/{message.guild.id}/{message.channel.id}/{message.id}" 9 | ) 10 | 11 | async def callback(self, interaction: discord.Interaction) -> None: 12 | pass 13 | 14 | class JumpToMessageView(discord.ui.View): 15 | def __init__(self, message: discord.message) -> None: 16 | super().__init__() 17 | self.add_item(JumpToMessageButton(message)) 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PotatoBot 2 | > The best AIO bot on Discord. 3 | 4 | top.gg: https://top.gg/bot/1226487228914602005 \ 5 | Invite: https://discord.com/oauth2/authorize?client_id=1226487228914602005 6 | 7 | 8 | ## Run Locally 9 | 10 | 1. Clone the project: 11 | 12 | ```bash 13 | git clone https://github.com/Cyteon/potatobot 14 | ``` 15 | 16 | 2. Go to the project directory: 17 | 18 | ```bash 19 | cd potatobot 20 | ``` 21 | 22 | 3. Install the required Python dependencies: 23 | 24 | ```bash 25 | pip install -r requirements.txt 26 | ``` 27 | 28 | 4. Rename `.env.example` to `.env`, and populate the required values. 29 | 5. Rename `config.example.json` to `config.json` and populate the required values. 30 | 31 | 6. Run the bot: 32 | 33 | ```bash 34 | python main.py 35 | ``` 36 | 37 | 7. Profit. 38 | -------------------------------------------------------------------------------- /ui/recreate.py: -------------------------------------------------------------------------------- 1 | import discord 2 | 3 | class deleteconfirm(discord.ui.View): 4 | def __init__(self, user, channel): 5 | super().__init__(timeout=None) 6 | self.user = user 7 | self.channel = channel 8 | 9 | @discord.ui.button(label="Confirm", style=discord.ButtonStyle.red) 10 | async def yes(self, interaction: discord.Interaction, button: discord.ui.Button): 11 | if interaction.user != self.user: 12 | return 13 | 14 | old_channel = self.channel 15 | 16 | await self.channel.delete() 17 | 18 | new_channel = await old_channel.clone() 19 | 20 | await new_channel.edit(position=old_channel.position) 21 | 22 | await new_channel.send("Channel has been recreated") 23 | 24 | @discord.ui.button(label="Cancel", style=discord.ButtonStyle.green) 25 | async def no(self, interaction: discord.Interaction, button: discord.ui.Button): 26 | await interaction.message.delete() 27 | -------------------------------------------------------------------------------- /utils/ServerLogger.py: -------------------------------------------------------------------------------- 1 | # This project is licensed under the terms of the GPL v3.0 license. Copyright 2024 Cyteon 2 | 3 | import discord 4 | 5 | from utils import DBClient 6 | client = DBClient.client 7 | db = client.potatobot 8 | 9 | async def send_log(title: str, guild: discord.Guild, description: str, color: discord.Color, channel: discord.TextChannel) -> None: 10 | c = db["guilds"] 11 | 12 | g = c.find_one({"id": guild.id}) 13 | 14 | if not g: 15 | await channel.send("**Log channel not found! If you are an admin use `/setting log_channel #channel`**") 16 | return 17 | 18 | if not g["log_channel"]: 19 | await channel.send("**Log channel not found! If you are an admin use `/setting log_channel #channel`**") 20 | return 21 | 22 | log_channel = g["log_channel"] 23 | log_channel = guild.get_channel(log_channel) 24 | 25 | embed = discord.Embed( 26 | title=title, 27 | description=description, 28 | color=color 29 | ) 30 | await log_channel.send(embed=embed) 31 | -------------------------------------------------------------------------------- /ui/translate.py: -------------------------------------------------------------------------------- 1 | import discord 2 | import aiohttp 3 | from discord import ui 4 | 5 | from deep_translator import GoogleTranslator 6 | 7 | class TranslateModal(ui.Modal, title = "Translate"): 8 | def __init__(self, message: discord.Message): 9 | super().__init__(timeout = 60) 10 | self.message = message 11 | 12 | language = ui.TextInput(label = "Language", placeholder = "Enter the language to translate this message to", style=discord.TextStyle.short, min_length = 1, max_length = 50) 13 | 14 | async def on_submit(self, interaction: discord.Interaction) -> None: 15 | try: 16 | translated = GoogleTranslator(source='auto', target=self.language.value.lower()).translate(self.message.content) 17 | 18 | embed = discord.Embed(title = "Translation", description = translated, color = discord.Color.blurple()) 19 | embed.set_footer(text = f"Original: \"{self.message.content}\"") 20 | 21 | await interaction.response.send_message(embed = embed, ephemeral = True) 22 | except: 23 | await interaction.response.send_message(content = "Failed to translate message", ephemeral = True) 24 | -------------------------------------------------------------------------------- /cogs/code.py: -------------------------------------------------------------------------------- 1 | """ 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2022 Ogiroid Development Team 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a 7 | copy of this software and associated documentation files (the "Software"), 8 | to deal in the Software without restriction, including without limitation 9 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 10 | and/or sell copies of the Software, and to permit persons to whom the 11 | Software is furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | """ 16 | 17 | from discord import app_commands 18 | from discord.ext import commands 19 | from discord.ext.commands import Context 20 | 21 | from utils import Checks 22 | from ui.code import CodeModal 23 | 24 | class Code(commands.Cog, name="💻 Code"): 25 | def __init__(self, bot) -> None: 26 | self.bot = bot 27 | 28 | @commands.hybrid_command( 29 | name="code", 30 | description="Run code in (almost) any language, a modal will pop up", 31 | usage="code" 32 | ) 33 | @commands.check(Checks.is_not_blacklisted) 34 | @commands.check(Checks.command_not_disabled) 35 | @app_commands.allowed_installs(guilds=True, users=True) 36 | @app_commands.allowed_contexts(guilds=True, dms=True, private_channels=True) 37 | async def code(self, context: Context) -> None: 38 | if not context.interaction: 39 | await context.send("This command can only be used as a slash command.") 40 | return 41 | await context.interaction.response.send_modal(CodeModal()) 42 | 43 | async def setup(bot) -> None: 44 | await bot.add_cog(Code(bot)) 45 | -------------------------------------------------------------------------------- /utils/Checks.py: -------------------------------------------------------------------------------- 1 | # This project is licensed under the terms of the GPL v3.0 license. Copyright 2024 Cyteon 2 | 3 | import discord 4 | 5 | from utils import DBClient, CONSTANTS, CachedDB, Errors 6 | 7 | from discord.ext import commands 8 | from discord.ext.commands import Context 9 | 10 | db = DBClient.db 11 | 12 | async def is_not_blacklisted(context: Context): 13 | users_global = db["users_global"] 14 | user = await CachedDB.find_one(users_global, {"id": context.author.id}) 15 | 16 | if user is None: 17 | user = CONSTANTS.user_global_data_template(context.author.id) 18 | users_global.insert_one(user) 19 | 20 | if user["blacklisted"]: 21 | raise Errors.UserBlacklisted("You are blacklisted from using the bot, reason: **" + (user["blacklist_reason"] if user["blacklist_reason"] else "Not Specified") + "**") 22 | else: 23 | return True 24 | 25 | # TODO: Add fakeperms 26 | def has_perm(**perms): 27 | def predicate(context: commands.Context): 28 | author_permissions = context.channel.permissions_for(context.author) 29 | 30 | for perm, value in perms.items(): 31 | if getattr(author_permissions, perm, None) != value: 32 | raise discord.ext.commands.MissingPermissions([perm]) 33 | return True 34 | 35 | return commands.check(predicate) 36 | 37 | async def command_not_disabled(context: Context): 38 | if context.guild: 39 | guild = await CachedDB.find_one(db["guilds"], {"id": context.guild.id}) 40 | 41 | if guild is None: 42 | guild = CONSTANTS.guild_data_template(context.guild.id) 43 | db["guilds"].insert_one(guild) 44 | 45 | if context.command.qualified_name in guild["disabled_commands"] or context.command.qualified_name.split(" ")[0] in guild["disabled_commands"]: 46 | raise Errors.CommandDisabled("This command is disabled in this server.") 47 | else: 48 | return True 49 | else: 50 | return True 51 | -------------------------------------------------------------------------------- /ui/code.py: -------------------------------------------------------------------------------- 1 | import discord 2 | import aiohttp 3 | from discord import ui 4 | 5 | class CodeModal(ui.Modal, title = "Run Code"): 6 | language = ui.TextInput(label = "Language", placeholder = "Enter the language of your code", style=discord.TextStyle.short, min_length = 1, max_length = 50) 7 | code = ui.TextInput(label = "Code", placeholder = "Enter your code here", style=discord.TextStyle.long, min_length = 1, max_length = 2000) 8 | 9 | async def on_submit(self, interaction: discord.Interaction) -> None: 10 | async with aiohttp.ClientSession() as session: 11 | embed = discord.Embed(title=f"Running your {self.language.value} code...", color=0xFFFFFF) 12 | 13 | code = self.code.value[:1000].strip() 14 | shortened = len(code) > 1000 15 | lines = code.splitlines() 16 | shortened = shortened or (len(lines) > 40) 17 | code = "\n".join(lines[:40]) 18 | code += shortened * "\n\n**Code shortened**" 19 | embed.add_field(name="Code", value=f"```{self.language.value}\n{code}```", inline=False) 20 | 21 | await interaction.response.send_message(embed=embed) 22 | 23 | response = await session.post( 24 | "https://emkc.org/api/v1/piston/execute", 25 | json={"language": self.language.value, "source": self.code.value}, 26 | ) 27 | 28 | json = await response.json() 29 | 30 | output = None 31 | try: 32 | output = json["output"] 33 | except KeyError: 34 | await interaction.response.send_message("An error occurred while running your code: \n\n" + json.get("message", "Unknown error")) 35 | return 36 | 37 | embed = discord.Embed(title=f"Ran your {json['language']} code", color=0xFFFFFF) 38 | output = output[:500].strip() 39 | shortened = len(output) > 500 40 | lines = output.splitlines() 41 | shortened = shortened or (len(lines) > 15) 42 | output = "\n".join(lines[:15]) 43 | output += shortened * "\n\n**Output shortened**" 44 | embed.add_field(name="Output", value=f"```{self.language.value}\n{output}\n```" or "****") 45 | 46 | await interaction.followup.send(embed=embed) 47 | -------------------------------------------------------------------------------- /cogs/giveaway.py: -------------------------------------------------------------------------------- 1 | # This project is licensed under the terms of the GPL v3.0 license. Copyright 2024 Cyteon 2 | 3 | import discord 4 | import random 5 | 6 | from discord.ext import commands 7 | from discord.ext.commands import Context 8 | 9 | from utils import Checks 10 | 11 | class Giveaway(commands.Cog, name="🎁 Giveaway"): 12 | def __init__(self, bot) -> None: 13 | self.bot = bot 14 | 15 | @commands.hybrid_group( 16 | name="giveaway", 17 | description="Command to start or end giveaways", 18 | usage="giveaway" 19 | ) 20 | @commands.check(Checks.is_not_blacklisted) 21 | @commands.check(Checks.command_not_disabled) 22 | @commands.has_permissions(manage_messages=True) 23 | async def giveaway(self, context: Context) -> None: 24 | prefix = await self.bot.get_prefix(context) 25 | 26 | cmds = "\n".join([f"{prefix}giveaway {cmd.name} - {cmd.description}" for cmd in self.giveaway.walk_commands()]) 27 | 28 | embed = discord.Embed( 29 | title=f"Help: Giveaway", description="List of available commands:", color=0xBEBEFE 30 | ) 31 | embed.add_field( 32 | name="Commands", value=f"```{cmds}```", inline=False 33 | ) 34 | 35 | await context.send(embed=embed) 36 | 37 | @giveaway.command( 38 | name="start", 39 | description="Start a giveaway!", 40 | usage="giveaway start " 41 | ) 42 | @commands.check(Checks.is_not_blacklisted) 43 | @commands.check(Checks.command_not_disabled) 44 | @commands.has_permissions(manage_messages=True) 45 | async def giveaway_start(self, context: Context, *, reward: str) -> None: 46 | embed = discord.Embed(title="Giveaway!", description=reward, color=0xBEBEFE) 47 | 48 | message = await context.send(embed=embed) 49 | 50 | await message.add_reaction("🎁") 51 | 52 | @giveaway.command( 53 | name = "end", 54 | description = "Ends a poll using message id", 55 | usage = "giveaway end " 56 | ) 57 | @commands.check(Checks.is_not_blacklisted) 58 | @commands.check(Checks.command_not_disabled) 59 | @commands.has_permissions(manage_messages=True) 60 | async def giveaway_end(self, context: Context, message_id) -> None: 61 | message_id = int(message_id) 62 | 63 | message = await context.fetch_message(message_id) 64 | 65 | users = [] 66 | 67 | async for u in message.reactions[0].users(): 68 | users.append(u) 69 | 70 | users.pop(users.index(self.bot.user)) 71 | winner = random.choice(users) 72 | 73 | embed = discord.Embed(title="Giveaway ended!", description="The winner is: {0} 🎉🎉🎉".format(winner.mention), color=0xBEBEFE) 74 | 75 | await message.reply(winner.mention, embed=embed) 76 | 77 | async def setup(bot) -> None: 78 | await bot.add_cog(Giveaway(bot)) 79 | -------------------------------------------------------------------------------- /.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 148 | 149 | graphs/ 150 | logs/ 151 | ssl/ 152 | pickle/ 153 | 154 | .pem 155 | .key 156 | config.json 157 | 158 | *.out 159 | -------------------------------------------------------------------------------- /utils/CachedDB.py: -------------------------------------------------------------------------------- 1 | # This project is licensed under the terms of the GPL v3.0 license. Copyright 2024 Cyteon 2 | 3 | import pymongo 4 | import redis 5 | import json 6 | import logging 7 | import time 8 | import os 9 | from bson import ObjectId 10 | 11 | logger = logging.getLogger("discord_bot") 12 | 13 | mongo_client_pool = pymongo.MongoClient(os.getenv("MONGODB_URL"), maxPoolSize=50) 14 | mongo_db = mongo_client_pool.potatobot 15 | 16 | redis_pool = redis.ConnectionPool.from_url(os.getenv("REDIS_URL"), max_connections=100) 17 | redis_client = redis.Redis(connection_pool=redis_pool) 18 | 19 | print("Connected to MongoDB at: ", mongo_client_pool.host) 20 | print("Connected to Redis at: ", redis_client.connection_pool.connection_kwargs["host"]) 21 | 22 | class JSONEncoder(json.JSONEncoder): 23 | def default(self, obj): 24 | if isinstance(obj, ObjectId): 25 | return str(obj) 26 | elif isinstance(obj, bytes): 27 | return None # Skip binary data 28 | return json.JSONEncoder.default(self, obj) 29 | 30 | async def find_one(collection, query, ex=30): 31 | start_time = time.time() * 1000 32 | 33 | cache_key = f"{collection.name}:{json.dumps(query, cls=JSONEncoder)}" 34 | cached_result = redis_client.get(cache_key) 35 | 36 | if cached_result: 37 | logger.info(f"Cache hit for query {cache_key} - took {time.time() * 1000 - start_time:.2f}ms") 38 | return json.loads(cached_result) 39 | else: 40 | result = collection.find_one(query) 41 | 42 | if result: 43 | result = json.loads(JSONEncoder().encode(result)) 44 | redis_client.set(cache_key, json.dumps(result), ex=ex) 45 | 46 | logger.info(f"Cache miss for query {cache_key} - took {time.time() * 1000 - start_time:.2f}ms") 47 | return result 48 | 49 | async def update_one(collection, filter, update, upsert=False): 50 | result = collection.update_one(filter, update, upsert=upsert) 51 | 52 | cache_key = f"{collection.name}:{json.dumps(filter, cls=JSONEncoder)}" 53 | redis_client.delete(cache_key) 54 | 55 | return result 56 | 57 | def sync_find_one(collection, query, ex=30): 58 | start_time = time.time() * 1000 59 | 60 | cache_key = f"{collection.name}:{json.dumps(query, cls=JSONEncoder)}" 61 | cached_result = redis_client.get(cache_key) 62 | 63 | if cached_result: 64 | logger.info(f"Cache hit for query {cache_key} - took {time.time() * 1000 - start_time:.2f}ms") 65 | return json.loads(cached_result) 66 | else: 67 | result = collection.find_one(query) 68 | 69 | if result: 70 | result = json.loads(JSONEncoder().encode(result)) 71 | redis_client.set(cache_key, json.dumps(result), ex=ex) 72 | 73 | logger.info(f"Cache miss for query {cache_key} - took {time.time() * 1000 - start_time:.2f}ms") 74 | return result 75 | 76 | def sync_update_one(collection, filter, update, upsert=False): 77 | result = collection.update_one(filter, update, upsert=upsert) 78 | 79 | cache_key = f"{collection.name}:{json.dumps(filter, cls=JSONEncoder)}" 80 | redis_client.delete(cache_key) 81 | 82 | return result 83 | -------------------------------------------------------------------------------- /utils/ErrorLogger.py: -------------------------------------------------------------------------------- 1 | # This project is licensed under the terms of the GPL v3.0 license. Copyright 2024 Cyteon 2 | 3 | import os 4 | import sys 5 | import json 6 | 7 | import discord 8 | from discord import Webhook 9 | import aiohttp 10 | import traceback 11 | 12 | if not os.path.isfile(f"./config.json"): 13 | sys.exit("'config.json' not found! Please add it and try again.") 14 | else: 15 | with open(f"./config.json") as file: 16 | config = json.load(file) 17 | 18 | def apply_context_errors(embed, context, ignore_message=False): 19 | embed.add_field( 20 | name="Author", 21 | value=f"{context.author.mention}", 22 | inline=True 23 | ) 24 | 25 | if context.guild: 26 | embed.add_field( 27 | name="Guild", 28 | value=f"`{context.guild.name}` (`{context.guild.id}`)", 29 | inline=True 30 | ) 31 | 32 | if context.command: 33 | embed.add_field( 34 | name="Command", 35 | value=f"`{context.command.name}`", 36 | inline=True 37 | ) 38 | 39 | if context.message.content != "" and not ignore_message: 40 | embed.add_field( 41 | name="Message", 42 | value=f"```{context.message.content}```", 43 | inline=True 44 | ) 45 | 46 | if context.interaction: 47 | options = context.interaction.data["options"] 48 | options = json.dumps(options, indent=2) 49 | 50 | embed.add_field( 51 | name="Interaction Options", 52 | value=f"```{options}```", 53 | inline=True 54 | ) 55 | 56 | async def command_error(error, context): 57 | async with aiohttp.ClientSession() as session: 58 | command_error_webhook = Webhook.from_url(config["command_error_webhook"], session=session) 59 | 60 | embed = discord.Embed( 61 | title="An error occurred!", 62 | description=f"```{error}```", 63 | color=discord.Color.red() 64 | ) 65 | 66 | apply_context_errors(embed, context) 67 | 68 | await command_error_webhook.send(embed=embed, username = "PotatoBot - Error Logger") 69 | 70 | async def error(self, event_method, *args, **kwargs): 71 | async with aiohttp.ClientSession() as session: 72 | error_webhook = Webhook.from_url(config["error_webhooks"], session=session) 73 | 74 | embed = discord.Embed( 75 | title="An error occurred!", 76 | description=f"```{traceback.format_exc().replace('```', '``')}```", 77 | color=discord.Color.red() 78 | ) 79 | 80 | embed.add_field( 81 | name="Event Method", 82 | value=f"`{event_method}`", 83 | inline=False 84 | ) 85 | 86 | if args: 87 | if isinstance(args[0], discord.ext.commands.Context): 88 | apply_context_errors(embed, args[0], ignore_message=True) 89 | else: 90 | embed.add_field( 91 | name="Args", 92 | value=f"```{args}```", 93 | inline=False 94 | ) 95 | 96 | embed.add_field( 97 | name="Kwargs", 98 | value=f"```{kwargs}```", 99 | inline=False 100 | ) 101 | 102 | await error_webhook.send(embed=embed, username="PotatoBot - Error Logger") 103 | -------------------------------------------------------------------------------- /cogs/api.py: -------------------------------------------------------------------------------- 1 | # This project is licensed under the terms of the GPL v3.0 license. Copyright 2024 Cyteon 2 | 3 | import discord 4 | import aiohttp 5 | 6 | from discord import app_commands 7 | from discord.ext import commands 8 | from discord.ext.commands import Context 9 | 10 | from utils import Checks 11 | 12 | class Api(commands.Cog, name="🌐 API"): 13 | def __init__(self, bot) -> None: 14 | self.bot = bot 15 | 16 | @commands.hybrid_group( 17 | name="api", 18 | description="Commands for diffrent APIs", 19 | usage="api [args]", 20 | ) 21 | @commands.check(Checks.is_not_blacklisted) 22 | @commands.check(Checks.command_not_disabled) 23 | @app_commands.allowed_installs(guilds=True, users=True) 24 | @app_commands.allowed_contexts(guilds=True, dms=True, private_channels=True) 25 | async def api(self, context: Context) -> None: 26 | prefix = await self.bot.get_prefix(context) 27 | 28 | cmds = "\n".join([f"{prefix}api {cmd.name} - {cmd.description}" for cmd in self.api.walk_commands()]) 29 | 30 | embed = discord.Embed( 31 | title=f"Help: Api", description="List of available commands:", color=0xBEBEFE 32 | ) 33 | embed.add_field( 34 | name="Commands", value=f"```{cmds}```", inline=False 35 | ) 36 | 37 | await context.send(embed=embed) 38 | 39 | @api.command( 40 | name="minecraft", 41 | aliases=["mc"], 42 | description="Get someones minecraft character", 43 | usage="api minecraft " 44 | ) 45 | @commands.check(Checks.is_not_blacklisted) 46 | @commands.check(Checks.command_not_disabled) 47 | async def api_minecraft(self, context: Context, *, username: str) -> None: 48 | embed = discord.Embed(title=f"Minecraft character for {username}", color=0xBEBEFE) 49 | embed.set_image(url=f"https://mc-heads.net/body/{username}") 50 | 51 | await context.send(embed=embed) 52 | 53 | @api.command( 54 | name="mc-server", 55 | aliases=["mcserver", "mc-srv", "mcs"], 56 | description="Get info on a minecraft server", 57 | usage="api mc-server " 58 | ) 59 | @commands.check(Checks.is_not_blacklisted) 60 | @commands.check(Checks.command_not_disabled) 61 | async def api_mc_server(self, context: Context, *, host: str) -> None: 62 | async with aiohttp.ClientSession() as session: 63 | async with session.get(f"https://api.mcsrvstat.us/3/{host}") as response: 64 | data = await response.json() 65 | 66 | if data["online"]: 67 | embed = discord.Embed( 68 | title=f"Server info for {host}", color=0xBEBEFE 69 | ) 70 | embed.add_field( 71 | name="Players", value=f"```{data['players']['online']}/{data['players']['max']}```", inline=False 72 | ) 73 | 74 | if "software" in data: 75 | embed.add_field( 76 | name="Version", value=f"```{data['version']} ({data['software']})```", inline=False 77 | ) 78 | else: 79 | embed.add_field( 80 | name="Version", value=f"```{data['version']}```", inline=False 81 | ) 82 | 83 | embed.add_field( 84 | name="MOTD", value=f"```{data['motd']['clean'][0]}```", inline=False 85 | ) 86 | 87 | if "list" in data["players"]: 88 | players = [p["name"] for p in data["players"]["list"]] 89 | players = ", ".join(players) 90 | 91 | 92 | embed.add_field( 93 | name="Online players", value=f"```{players}```", inline=False 94 | ) 95 | 96 | if "plugins" in data: 97 | plugins = [p["name"] for p in data["plugins"]] 98 | plugins = ", ".join(plugins) 99 | 100 | embed.add_field( 101 | name="Plugins", value=f"```{plugins}```", inline=False 102 | ) 103 | 104 | if "mods" in data: 105 | mods = [m["name"] for m in data["mods"]] 106 | mods = ", ".join(mods) 107 | 108 | embed.add_field( 109 | name="Mods", value=f"```{mods}```", inline=False 110 | ) 111 | 112 | await context.send(embed=embed) 113 | else: 114 | await context.send("The server is offline") 115 | 116 | async def setup(bot) -> None: 117 | await bot.add_cog(Api(bot)) 118 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | # This project is licensed under the terms of the GPL v3.0 license. Copyright 2024 Cyteon 2 | 3 | import os 4 | import threading 5 | import uvicorn 6 | import json 7 | import sys 8 | from bson import ObjectId 9 | from dotenv import load_dotenv 10 | from typing import Optional 11 | 12 | import ssl 13 | 14 | from fastapi import FastAPI 15 | from fastapi.middleware.cors import CORSMiddleware 16 | 17 | from bot import DiscordBot 18 | 19 | from utils import DBClient, CONSTANTS, CachedDB 20 | 21 | db = DBClient.db 22 | 23 | # Load environment variables 24 | load_dotenv() 25 | 26 | # Instantiate the bot and FastAPI app 27 | bot = DiscordBot() 28 | app = FastAPI() 29 | 30 | if not os.path.isfile(f"{os.path.realpath(os.path.dirname(__file__))}/config.json"): 31 | sys.exit("'config.json' not found! Please add it and try again.") 32 | else: 33 | with open(f"{os.path.realpath(os.path.dirname(__file__))}/config.json") as file: 34 | config = json.load(file) 35 | 36 | origins = config["origins"] 37 | 38 | app.add_middleware( 39 | CORSMiddleware, 40 | allow_origins=origins, 41 | allow_credentials=True, 42 | allow_methods=["*"], 43 | allow_headers=["*"], 44 | ) 45 | 46 | class JSONEncoder(json.JSONEncoder): 47 | def default(self, obj): 48 | if isinstance(obj, ObjectId): 49 | return str(obj) 50 | elif isinstance(obj, bytes): 51 | return None # Skip binary data 52 | return json.JSONEncoder.default(self, obj) 53 | 54 | @app.get("/") 55 | async def read_root(): 56 | return {"message": "User: " + bot.user.name + " is online! "} 57 | 58 | @app.get("/api") 59 | async def read_api_root(): 60 | return {"message": "OK"} 61 | 62 | @app.get("/api/commands/{cog}") 63 | async def get_commands(cog: Optional[str] = "all"): 64 | if cog == "all": 65 | all_commands = [ 66 | { 67 | "name": (cmd.parent.name + " " if cmd.parent else "") + cmd.name, 68 | "description": cmd.description, 69 | "cog": cmd.cog_name, 70 | "usage": cmd.usage, "aliases": cmd.aliases, 71 | "subcommand": cmd.parent != None, 72 | "extras": cmd.extras 73 | } for cmd in bot.walk_commands() if not "owner" in cmd.cog_name 74 | ] 75 | return all_commands 76 | else: 77 | if cog not in bot.cogs: 78 | return {"message": "Cog not found.", "status": 404} 79 | 80 | if "owner" in cog: 81 | return {"message": "Cog not found.", "status": 404} 82 | 83 | commands = [ 84 | { 85 | "name": (cmd.parent.name + " " if cmd.parent else "") + cmd.name, 86 | "description": cmd.description, 87 | "usage": cmd.usage, "aliases": cmd.aliases, 88 | "subcommand": cmd.parent != None, 89 | "extras": cmd.extras 90 | } for cmd in bot.get_cog(cog).walk_commands() 91 | ] 92 | 93 | return commands 94 | 95 | @app.get("/api/cogs") 96 | async def get_cogs(): 97 | cogs = list(bot.cogs.keys()) 98 | if 'owner' in cogs: 99 | cogs.remove('owner') 100 | return cogs 101 | 102 | @app.get("/api/guild/{id}") 103 | async def get_guild(id: int): 104 | guild = bot.get_guild(id) 105 | 106 | if guild is None: 107 | return {"message": "Guild not found.", "status": 404} 108 | 109 | guilds = db["guilds"] 110 | guild_data = guilds.find_one({"id": guild.id}) 111 | 112 | if guild_data is None: 113 | guild_data = CONSTANTS.guild_data_template(id) 114 | guilds.insert_one(guild_data) 115 | 116 | guild = { 117 | "name": guild.name, 118 | "id": guild.id, 119 | "dbdata": str(JSONEncoder().encode(guild_data)), 120 | "members": len(guild.members), 121 | "channels": len(guild.channels), 122 | "roles": len(guild.roles), 123 | } 124 | 125 | return guild 126 | 127 | 128 | @app.get("/api/user/{id}") 129 | async def get_user(id: int): 130 | user = bot.get_user(id) 131 | 132 | if user is None: 133 | return {"message": "User not found.", "status": 404} 134 | 135 | users = db["global_users"] 136 | user_data = await CachedDB.find_one(users, {"id": user.id}) 137 | 138 | if user_data is None: 139 | user_data = CONSTANTS.user_global_data_template(id) 140 | users.insert_one(user_data) 141 | 142 | if user_data["blacklisted"]: 143 | return {"message": "User is blacklisted.", "status": 403, "reason": user_data["blacklist_reason"]} 144 | 145 | mutals = user.mutual_guilds 146 | 147 | guilds = [] 148 | 149 | for guild in mutals: 150 | if guild.get_member(user.id).guild_permissions.administrator: 151 | guilds.append({ 152 | "name": guild.name, 153 | "id": str(guild.id), 154 | "members": len(guild.members), 155 | }) 156 | 157 | return { 158 | "name": user.name, 159 | "id": user.id, 160 | "guilds": guilds 161 | } 162 | 163 | @app.get("/api/stats") 164 | async def get_stats(): 165 | return { 166 | "commands_ran": bot.statsDB.get("commands_ran"), 167 | "users": len(set(bot.get_all_members())), 168 | "ai_requests": bot.statsDB.get("ai_requests"), 169 | } 170 | 171 | def run_fastapi(): 172 | if config["use_ssl"]: 173 | uvicorn.run( 174 | app, host="0.0.0.0", 175 | port=config["port"], 176 | ssl_keyfile=config["ssl_keyfile"], 177 | ssl_certfile=config["ssl_certfile"], 178 | ssl_version=ssl.PROTOCOL_TLS 179 | ) 180 | else: 181 | uvicorn.run( 182 | app, host="0.0.0.0", 183 | port=config["port"], 184 | ) 185 | 186 | thread = threading.Thread(target=run_fastapi) 187 | thread.start() 188 | 189 | TOKEN = os.getenv("TOKEN") 190 | bot.run(TOKEN) 191 | -------------------------------------------------------------------------------- /utils/CONSTANTS.py: -------------------------------------------------------------------------------- 1 | # This project is licensed under the terms of the GPL v3.0 license. Copyright 2024 Cyteon 2 | 3 | from __future__ import annotations 4 | 5 | from dataclasses import dataclass 6 | 7 | from typing import Final 8 | 9 | def guild_data_template(guild_id): 10 | return { 11 | "id": guild_id, 12 | "level_roles": {}, 13 | "daily_cash": 50, 14 | "tickets_category": 0, 15 | "tickets_support_role": 0, 16 | "log_channel": 0, 17 | "ai_access": False, 18 | "should_announce_levelup": False, 19 | "groq_api_key": "NONE", 20 | "level_announce_channel": 0, 21 | "jail_role": 0, 22 | "jail_channel": 0, 23 | "default_role": 0, 24 | "lockdown": False, 25 | "oldperms": {}, 26 | "fakeperms": {}, 27 | "authorized_bots": [], 28 | "disabled_commands": [], 29 | "starboard": { 30 | "channel": 0, 31 | "threshold": 5, 32 | "enabled": False 33 | }, 34 | "security": { 35 | "antinuke": { 36 | "anti_danger_perms": False, 37 | "anti_massban": False, 38 | "anti_masskick": False, 39 | "anti_massdelete": False, 40 | "anti_massping": False, 41 | "anti_webhook_spam": False, 42 | "anti_unauthorized_bot": False, 43 | } 44 | } 45 | } 46 | 47 | def user_data_template(user_id, guild_id): 48 | return { 49 | "id": user_id, 50 | "guild_id": guild_id, 51 | "wallet": 0, 52 | "xp": 0, 53 | "level": 0, 54 | "last_daily": 0, 55 | "last_robbed_at": 0, 56 | "jailed": False, 57 | "farm": { 58 | "saplings": 0, 59 | "crops": 0, 60 | "harvestable": 0, 61 | "ready_in": 0 62 | }, 63 | "warnings": [], 64 | "whitelisted": False, 65 | "trusted": False 66 | } 67 | 68 | def user_global_data_template(user_id): 69 | return { 70 | "id": user_id, 71 | "blacklisted": False, 72 | "blacklist_reason": "", 73 | "ai_ignore": False, 74 | "ai_ignore_reason": "", 75 | "inspect": { 76 | "total_commands": 0, 77 | "times_flagged": 0, 78 | "nsfw_requests": 0, 79 | "ai_requests": 0, 80 | }, 81 | "strikes": {} 82 | } 83 | 84 | LEVELS_AND_XP: Final = { # credit's for this goes to the mee6 developers as we use the same xp values as them 85 | 0: 0, 86 | 1: 100, 87 | 2: 255, 88 | 3: 475, 89 | 4: 770, 90 | 5: 1_150, 91 | 6: 1_625, 92 | 7: 2_205, 93 | 8: 2_900, 94 | 9: 3_720, 95 | 10: 4_675, 96 | 11: 5_775, 97 | 12: 7_030, 98 | 13: 8_450, 99 | 14: 10_045, 100 | 15: 11_825, 101 | 16: 13_800, 102 | 17: 15_980, 103 | 18: 18_375, 104 | 19: 20_995, 105 | 20: 23_850, 106 | 21: 26_950, 107 | 22: 30_305, 108 | 23: 33_925, 109 | 24: 37_820, 110 | 25: 42_000, 111 | 26: 46_475, 112 | 27: 51_255, 113 | 28: 56_350, 114 | 29: 61_770, 115 | 30: 67_525, 116 | 31: 73_625, 117 | 32: 80_080, 118 | 33: 86_900, 119 | 34: 94_095, 120 | 35: 101_675, 121 | 36: 109_650, 122 | 37: 118_030, 123 | 38: 126_825, 124 | 39: 136_045, 125 | 40: 145_700, 126 | 41: 155_800, 127 | 42: 166_355, 128 | 43: 177_375, 129 | 44: 188_870, 130 | 45: 200_850, 131 | 46: 213_325, 132 | 47: 226_305, 133 | 48: 239_800, 134 | 49: 253_820, 135 | 50: 268_375, 136 | 51: 283_475, 137 | 52: 299_130, 138 | 53: 315_350, 139 | 54: 332_145, 140 | 55: 349_525, 141 | 56: 367_500, 142 | 57: 386_080, 143 | 58: 405_275, 144 | 59: 425_095, 145 | 60: 445_550, 146 | 61: 466_650, 147 | 62: 488_405, 148 | 63: 510_825, 149 | 64: 533_920, 150 | 65: 557_700, 151 | 66: 582_175, 152 | 67: 607_355, 153 | 68: 633_250, 154 | 69: 659_870, 155 | 70: 687_225, 156 | 71: 715_325, 157 | 72: 744_180, 158 | 73: 773_800, 159 | 74: 804_195, 160 | 75: 835_375, 161 | 76: 867_350, 162 | 77: 900_130, 163 | 78: 933_725, 164 | 79: 968_145, 165 | 80: 1_003_400, 166 | 81: 1_039_500, 167 | 82: 1_076_455, 168 | 83: 1_114_275, 169 | 84: 1_152_970, 170 | 85: 1_192_550, 171 | 86: 1_233_025, 172 | 87: 1_274_405, 173 | 88: 1_316_700, 174 | 89: 1_359_920, 175 | 90: 1_404_075, 176 | 91: 1_449_175, 177 | 92: 1_495_230, 178 | 93: 1_542_250, 179 | 94: 1_590_245, 180 | 95: 1_639_225, 181 | 96: 1_689_200, 182 | 97: 1_740_180, 183 | 98: 1_792_175, 184 | 99: 1_845_195, 185 | 100: 1_899_250, 186 | 101: 1_954_350, 187 | 102: 2_010_505, 188 | 103: 2_067_725, 189 | 104: 2_126_020, 190 | 105: 2_185_400, 191 | 106: 2_245_875, 192 | 107: 2_307_455, 193 | 108: 2_370_150, 194 | 109: 2_433_970, 195 | 110: 2_498_925, 196 | 111: 2_565_025, 197 | 112: 2_632_280, 198 | 113: 2_700_700, 199 | 114: 2_770_295, 200 | 115: 2_841_075, 201 | 116: 2_913_050, 202 | 117: 2_986_230, 203 | 118: 3_060_625, 204 | 119: 3_136_245, 205 | 120: 3_213_100, 206 | 121: 3_291_200, 207 | 122: 3_370_555, 208 | 123: 3_451_175, 209 | 124: 3_533_070, 210 | 125: 3_616_250, 211 | 126: 3_700_725, 212 | 127: 3_786_505, 213 | 128: 3_873_600, 214 | 129: 3_962_020, 215 | 130: 4_051_775, 216 | 131: 4_142_875, 217 | 132: 4_235_330, 218 | 133: 4_329_150, 219 | 134: 4_424_345, 220 | 135: 4_520_925, 221 | 136: 4_618_900, 222 | 137: 4_718_280, 223 | 138: 4_819_075, 224 | 139: 4_921_295, 225 | 140: 5_024_950, 226 | 141: 5_130_050, 227 | 142: 5_236_605, 228 | 143: 5_344_625, 229 | 144: 5_454_120, 230 | 145: 5_565_100, 231 | 146: 5_677_575, 232 | 147: 5_791_555, 233 | 148: 5_907_050, 234 | 149: 6_024_070, 235 | 150: 6_142_625, 236 | } 237 | MAX_LEVEL: Final = len(LEVELS_AND_XP) - 1 238 | MAX_XP: Final = LEVELS_AND_XP[MAX_LEVEL] 239 | -------------------------------------------------------------------------------- /cogs/reactionroles.py: -------------------------------------------------------------------------------- 1 | # This project is licensed under the terms of the GPL v3.0 license. Copyright 2024 Cyteon 2 | 3 | import discord 4 | 5 | from discord.ext import commands 6 | from discord.ext.commands import Context 7 | 8 | from utils import CachedDB, DBClient 9 | 10 | db = DBClient.db 11 | 12 | from utils import Checks 13 | 14 | class ReactionRoles(commands.Cog, name="🇺🇸 Reaction Roles"): 15 | def __init__(self, bot) -> None: 16 | self.bot = bot 17 | 18 | @commands.Cog.listener() 19 | async def on_raw_reaction_add(self, payload) -> None: 20 | message_data = await CachedDB.find_one(db["reaction_roles"], {"message_id": payload.message_id}) 21 | if not message_data: 22 | return 23 | 24 | # Determine the emoji identifier 25 | if payload.emoji.id is None: 26 | # This is a Unicode emoji 27 | emoji_id = str(payload.emoji) 28 | else: 29 | # This is a custom emoji 30 | emoji_id = str(payload.emoji.id) 31 | 32 | if emoji_id not in message_data["roles"]: 33 | return 34 | 35 | guild = self.bot.get_guild(payload.guild_id) 36 | if not guild: 37 | return 38 | 39 | role = guild.get_role(int(message_data["roles"][emoji_id])) 40 | if not role: 41 | return 42 | 43 | member = guild.get_member(payload.user_id) 44 | if not member: 45 | return 46 | 47 | try: 48 | await member.add_roles(role) 49 | print(f"Added role {role.name} to {member.name}") 50 | except discord.HTTPException as e: 51 | print(f"Failed to add role: {e}") 52 | 53 | @commands.Cog.listener() 54 | async def on_raw_reaction_remove(self, payload) -> None: 55 | message_data = await CachedDB.find_one(db["reaction_roles"], {"message_id": payload.message_id}) 56 | if not message_data: 57 | return 58 | 59 | # Determine the emoji identifier 60 | if payload.emoji.id is None: 61 | # This is a Unicode emoji 62 | emoji_id = str(payload.emoji) 63 | else: 64 | # This is a custom emoji 65 | emoji_id = str(payload.emoji.id) 66 | 67 | if emoji_id not in message_data["roles"]: 68 | try: 69 | message = await self.bot.get_channel(payload.channel_id).fetch_message(payload.message_id) 70 | await message.remove_reaction(payload.emoji, self.bot.get_user(payload.user_id)) 71 | except Exception as e: 72 | print(e) 73 | pass 74 | return 75 | 76 | guild = self.bot.get_guild(payload.guild_id) 77 | if not guild: 78 | return 79 | 80 | role = guild.get_role(int(message_data["roles"][emoji_id])) 81 | if not role: 82 | return 83 | 84 | member = guild.get_member(payload.user_id) 85 | if not member: 86 | return 87 | 88 | try: 89 | await member.remove_roles(role) 90 | print(f"Removed role {role.name} from {member.name}") 91 | except discord.HTTPException as e: 92 | print(f"Failed to remove role: {e}") 93 | 94 | @commands.hybrid_group( 95 | name="reactionroles", 96 | description="Command to manage reaction roles", 97 | usage="reactionroles", 98 | aliases=["rr"] 99 | ) 100 | @commands.check(Checks.is_not_blacklisted) 101 | @commands.check(Checks.command_not_disabled) 102 | @commands.has_permissions(manage_roles=True) 103 | async def reactionroles(self, context: Context) -> None: 104 | subcommands = [cmd for cmd in self.reactionroles.walk_commands()] 105 | 106 | data = [] 107 | 108 | for subcommand in subcommands: 109 | description = subcommand.description.partition("\n")[0] 110 | data.append(f"{await self.bot.get_prefix(context)}reactionroles {subcommand.name} - {description}") 111 | 112 | help_text = "\n".join(data) 113 | embed = discord.Embed( 114 | title=f"Help: Reaction Roles", description="List of available commands:", color=0xBEBEFE 115 | ) 116 | embed.add_field( 117 | name="Commands", value=f"```{help_text}```", inline=False 118 | ) 119 | 120 | await context.send(embed=embed) 121 | 122 | @reactionroles.command( 123 | name="add", 124 | description="Add a reaction role to a message", 125 | usage="reactionroles add ", 126 | ) 127 | @commands.check(Checks.is_not_blacklisted) 128 | @commands.check(Checks.command_not_disabled) 129 | @commands.has_permissions(manage_roles=True) 130 | async def add(self, context: commands.Context, message_id: str, role: discord.Role, emoji: str): 131 | try: 132 | message_id = int(message_id) 133 | except: 134 | await context.send("Invalid message ID.") 135 | return 136 | 137 | try: 138 | message = await context.channel.fetch_message(message_id) 139 | except discord.NotFound: 140 | await context.send("Message not found.") 141 | return 142 | 143 | # Convert emoji to a format we can use 144 | try: 145 | # Try to convert to Discord emoji 146 | emoji_obj = await commands.EmojiConverter().convert(context, emoji) 147 | emoji_id = str(emoji_obj.id) 148 | except commands.BadArgument: 149 | # If conversion fails, assume it's a Unicode emoji 150 | emoji_id = emoji 151 | 152 | try: 153 | await message.add_reaction(emoji) 154 | except discord.HTTPException: 155 | await context.send("Failed to add reaction. Make sure the bot has permission to add reactions.") 156 | return 157 | 158 | message_data = await CachedDB.find_one(db["reaction_roles"], {"message_id": message_id}) 159 | 160 | if not message_data: 161 | db["reaction_roles"].insert_one({ 162 | "message_id": message_id, 163 | "roles": {emoji_id: str(role.id)} 164 | }) 165 | await context.send("Reaction role added.") 166 | else: 167 | if emoji_id in message_data["roles"]: 168 | await context.send("Reaction role already exists.") 169 | else: 170 | message_data["roles"][emoji_id] = str(role.id) 171 | await CachedDB.update_one(db["reaction_roles"], {"message_id": message_id}, {"$set": {"roles": message_data["roles"]}}) 172 | await context.send("Reaction role added.") 173 | 174 | async def setup(bot) -> None: 175 | await bot.add_cog(ReactionRoles(bot)) 176 | -------------------------------------------------------------------------------- /cogs/github.py: -------------------------------------------------------------------------------- 1 | """ 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2022 Ogiroid Development Team 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a 7 | copy of this software and associated documentation files (the "Software"), 8 | to deal in the Software without restriction, including without limitation 9 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 10 | and/or sell copies of the Software, and to permit persons to whom the 11 | Software is furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | """ 16 | 17 | 18 | import aiohttp 19 | import discord 20 | from discord import app_commands 21 | from discord.ext import commands 22 | from discord.ext.commands import Context 23 | 24 | from utils import Checks 25 | 26 | 27 | class Github(commands.Cog, name="🖧 Github"): 28 | def __init__(self, bot) -> None: 29 | self.bot = bot 30 | 31 | @commands.hybrid_group( 32 | name="github", 33 | description="Commands related to GitHub", 34 | usage="github [args]", 35 | aliases=["gh"], 36 | ) 37 | @commands.check(Checks.is_not_blacklisted) 38 | @commands.check(Checks.command_not_disabled) 39 | @app_commands.allowed_installs(guilds=True, users=True) 40 | @app_commands.allowed_contexts(guilds=True, dms=True, private_channels=True) 41 | async def github(self, context: Context) -> None: 42 | prefix = await self.bot.get_prefix(context) 43 | 44 | cmds = "\n".join([f"{prefix}github {cmd.name} - {cmd.description}" for cmd in self.github.walk_commands()]) 45 | 46 | embed = discord.Embed( 47 | title=f"Help: Github", description="List of available commands:", color=0xBEBEFE 48 | ) 49 | embed.add_field( 50 | name="Commands", value=f"```{cmds}```", inline=False 51 | ) 52 | 53 | await context.send(embed=embed) 54 | 55 | # Command to get information about a GitHub user 56 | @github.command( 57 | name="user", 58 | description="Gets the Profile of the github person.", 59 | usage="github user " 60 | ) 61 | @commands.check(Checks.is_not_blacklisted) 62 | @commands.check(Checks.command_not_disabled) 63 | async def ghuser(self, context, user: str): 64 | async with aiohttp.ClientSession() as session: 65 | person_raw = await session.get( 66 | f"https://api.github.com/users/{user}" 67 | ) 68 | if person_raw.status != 200: 69 | return await context.send("User not found!") 70 | else: 71 | person = await person_raw.json() 72 | # Returning an Embed containing all the information: 73 | embed = discord.Embed( 74 | title=f"GitHub Profile: {person['login']}", 75 | description=f"**Bio:** {person['bio']}", 76 | color=0xFFFFFF, 77 | ) 78 | embed.set_thumbnail(url=f"{person['avatar_url']}") 79 | embed.add_field( 80 | name="Username 📛: ", value=f"{person['name']}", inline=True 81 | ) 82 | # embed.add_field(name="Email ✉: ", value=f"{person['email']}", inline=True) Commented due to GitHub not responding with the correct email 83 | embed.add_field( 84 | name="Repos 📁: ", value=f"{person['public_repos']}", inline=True 85 | ) 86 | embed.add_field( 87 | name="Location 📍: ", value=f"{person['location']}", inline=True 88 | ) 89 | embed.add_field( 90 | name="Company 🏢: ", value=f"{person['company']}", inline=True 91 | ) 92 | embed.add_field( 93 | name="Followers 👥: ", value=f"{person['followers']}", inline=True 94 | ) 95 | embed.add_field( 96 | name="Website 🖥️: ", value=f"{person['blog']}", inline=True 97 | ) 98 | 99 | await context.send(embed=embed, view=ProfileButton(url=person["html_url"])) 100 | 101 | # Command to get search for GitHub repositories: 102 | @github.command( 103 | name="repo", 104 | description="Searches for the specified repo.", 105 | usage="github repo " 106 | ) 107 | @commands.check(Checks.is_not_blacklisted) 108 | @commands.check(Checks.command_not_disabled) 109 | async def ghsearchrepo(self, context, query: str): 110 | pages = 1 111 | url = f"https://api.github.com/search/repositories?q={query}&{pages}" 112 | async with aiohttp.ClientSession() as session: 113 | repos_raw = await session.get(url) 114 | if repos_raw.status != 200: 115 | return await context.send("Repo not found!") 116 | else: 117 | repos = ( 118 | await repos_raw.json() 119 | ) # Getting first repository from the query 120 | repo = repos["items"][0] 121 | # Returning an Embed containing all the information: 122 | embed = discord.Embed( 123 | title=f"GitHub Repository: {repo['name']}", 124 | description=f"**Description:** {repo['description']}", 125 | color=0xFFFFFF, 126 | ) 127 | embed.set_thumbnail(url=f"{repo['owner']['avatar_url']}") 128 | embed.add_field( 129 | name="Author 🖊:", 130 | value=f"__[{repo['owner']['login']}]({repo['owner']['html_url']})__", 131 | inline=True, 132 | ) 133 | embed.add_field( 134 | name="Stars ⭐:", value=f"{repo['stargazers_count']}", inline=True 135 | ) 136 | embed.add_field( 137 | name="Forks 🍴:", value=f"{repo['forks_count']}", inline=True 138 | ) 139 | embed.add_field( 140 | name="Language 💻:", value=f"{repo['language']}", inline=True 141 | ) 142 | embed.add_field( 143 | name="Size 🗃️:", 144 | value=f"{round(repo['size'] / 1000, 2)} MB", 145 | inline=True, 146 | ) 147 | if repo["license"]: 148 | spdx_id = repo["license"]["spdx_id"] 149 | embed.add_field( 150 | name="License name 📃:", 151 | value=f"{spdx_id if spdx_id != 'NOASSERTION' else repo['license']['name']}", 152 | inline=True, 153 | ) 154 | else: 155 | embed.add_field( 156 | name="License name 📃:", 157 | value="This Repo doesn't have a license", 158 | inline=True, 159 | ) 160 | 161 | await context.send(embed=embed, view=RepoButton(url=repo["html_url"])) 162 | 163 | class ProfileButton(discord.ui.View): 164 | def __init__(self, url: str): 165 | super().__init__() 166 | 167 | self.add_item(discord.ui.Button(label="GitHub Profile", style=discord.ButtonStyle.url, url=url)) 168 | 169 | class RepoButton(discord.ui.View): 170 | def __init__(self, url: str): 171 | super().__init__() 172 | 173 | self.add_item(discord.ui.Button(label="GitHub Repository", style=discord.ButtonStyle.url, url=url)) 174 | 175 | async def setup(bot) -> None: 176 | await bot.add_cog(Github(bot)) 177 | -------------------------------------------------------------------------------- /cogs/utility.py: -------------------------------------------------------------------------------- 1 | # This project is licensed under the terms of the GPL v3.0 license. Copyright 2024 Cyteon 2 | 3 | import io 4 | 5 | from asteval import Interpreter 6 | aeval = Interpreter() 7 | 8 | import discord 9 | from discord import app_commands 10 | from discord.ext import commands 11 | from discord.ext.commands import Context 12 | from deep_translator import GoogleTranslator 13 | 14 | from utils import Checks 15 | 16 | from PIL import ImageColor, Image 17 | 18 | class Utility(commands.Cog, name="⚡ Utility"): 19 | def __init__(self, bot) -> None: 20 | self.bot = bot 21 | 22 | @commands.hybrid_group( 23 | name="convert", 24 | description="Commands to convert stuff", 25 | usage="convert " 26 | ) 27 | @commands.check(Checks.is_not_blacklisted) 28 | @commands.check(Checks.command_not_disabled) 29 | @app_commands.allowed_installs(guilds=True, users=True) 30 | @app_commands.allowed_contexts(guilds=True, dms=True, private_channels=True) 31 | async def convert(self, context: Context) -> None: 32 | prefix = await self.bot.get_prefix(context) 33 | 34 | cmds = "\n".join([f"{prefix}convert {cmd.name} - {cmd.description}" for cmd in self.convert.walk_commands()]) 35 | 36 | embed = discord.Embed( 37 | title=f"Help: Convert", description="List of available commands:", color=0xBEBEFE 38 | ) 39 | embed.add_field( 40 | name="Commands", value=f"```{cmds}```", inline=False 41 | ) 42 | 43 | await context.send(embed=embed) 44 | 45 | @convert.command( 46 | name="mb-gb", 47 | aliases=["mbgb", "mb-to-gb", "mb2gb"], 48 | description="Convert megabytes to gigabytes", 49 | ) 50 | @commands.check(Checks.is_not_blacklisted) 51 | @commands.check(Checks.command_not_disabled) 52 | async def convert_mb_gb(self, context: Context, mb: float, binary: bool = True) -> None: 53 | if binary: 54 | gb = mb / 1024 55 | else: 56 | gb = mb / 1000 57 | 58 | await context.send(f"{mb}MB is equal to {gb}GB") 59 | 60 | @convert.command( 61 | name="gb-mb", 62 | aliases=["gbmb", "gb-to-mb", "gb2mb"], 63 | description="Convert gigabytes to megabytes", 64 | ) 65 | @commands.check(Checks.is_not_blacklisted) 66 | @commands.check(Checks.command_not_disabled) 67 | async def convert_gb_mb(self, context: Context, gb: float, binary: bool = True) -> None: 68 | if binary: 69 | mb = gb * 1024 70 | else: 71 | mb = gb * 1000 72 | 73 | await context.send(f"{gb}GB is equal to {mb}MB") 74 | 75 | @convert.command( 76 | name="gb-tb", 77 | aliases=["gbtb", "gb-to-tb", "gb2tb"], 78 | description="Convert gigabytes to terabytes", 79 | ) 80 | @commands.check(Checks.is_not_blacklisted) 81 | @commands.check(Checks.command_not_disabled) 82 | async def convert_gb_tb(self, context: Context, gb: float, binary: bool = True) -> None: 83 | if binary: 84 | tb = gb / 1024 85 | else: 86 | tb = gb / 1000 87 | 88 | await context.send(f"{gb}GB is equal to {tb}TB") 89 | 90 | @convert.command( 91 | name="tb-gb", 92 | aliases=["tbg", "tb-to-gb", "tb2gb"], 93 | description="Convert terabytes to gigabytes", 94 | ) 95 | @commands.check(Checks.is_not_blacklisted) 96 | @commands.check(Checks.command_not_disabled) 97 | async def convert_tb_gb(self, context: Context, tb: float, binary: bool = True) -> None: 98 | if binary: 99 | gb = tb * 1024 100 | else: 101 | gb = tb * 1000 102 | 103 | await context.send(f"{tb}TB is equal to {gb}GB") 104 | 105 | @commands.hybrid_command( 106 | name="calc", 107 | description="Calculate a math expression.", 108 | aliases=["calculate"], 109 | usage="calc ", 110 | ) 111 | @commands.check(Checks.is_not_blacklisted) 112 | @commands.check(Checks.command_not_disabled) 113 | @app_commands.allowed_installs(guilds=True, users=True) 114 | @app_commands.allowed_contexts(guilds=True, dms=True, private_channels=True) 115 | async def calc(self, context: Context, *, expression: str) -> None: 116 | try: 117 | result = aeval(expression) 118 | 119 | embed = discord.Embed( 120 | title="Calculator", 121 | description=f"**Input:**\n```{expression}```\n**Output:**\n```{result}```", 122 | color=0xBEBEFE 123 | ) 124 | 125 | await context.send(embed=embed) 126 | except Exception as e: 127 | await context.send(f"An error occurred: {e}") 128 | 129 | @commands.hybrid_command( 130 | name="translate", 131 | description="Translate text to a specified language example: ,translate en hola", 132 | usage="translate " 133 | ) 134 | @commands.check(Checks.is_not_blacklisted) 135 | @commands.check(Checks.command_not_disabled) 136 | @app_commands.describe(text="The text you want to translate.") 137 | @app_commands.describe(language="The language you want to translate the text to.") 138 | @app_commands.allowed_installs(guilds=True, users=True) 139 | @app_commands.allowed_contexts(guilds=True, dms=True, private_channels=True) 140 | async def translate(self, context: Context, language, *, text: str) -> None: 141 | translated = GoogleTranslator(source='auto', target=language).translate(text) 142 | 143 | embed = discord.Embed( 144 | title="Translation", 145 | description=f"**Original text:**\n{text}\n\n**Translated text:**\n{translated}", 146 | color=0xBEBEFE, 147 | ) 148 | embed.set_footer(text=f"Translated to {language}") 149 | await context.send(embed=embed) 150 | 151 | @commands.hybrid_command( 152 | name="color", 153 | description="Get information about a color.", 154 | aliases=["colour"], 155 | usage="color " 156 | ) 157 | @commands.check(Checks.is_not_blacklisted) 158 | @commands.check(Checks.command_not_disabled) 159 | @app_commands.allowed_installs(guilds=True, users=True) 160 | @app_commands.allowed_contexts(guilds=True, dms=True, private_channels=True) 161 | async def color(self, context: Context, color: str) -> None: 162 | try: 163 | rgb = ImageColor.getrgb(color) 164 | rgba = ImageColor.getcolor(color, "RGBA") 165 | grayscale = ImageColor.getcolor(color, "L") 166 | hex = hex_value = "#{:02x}{:02x}{:02x}".format(*rgb) 167 | 168 | 169 | embed = discord.Embed( 170 | title="Color Information", 171 | description="\n".join( 172 | [ 173 | f"Color: **{color}**", 174 | f"Hex: **{hex}**", 175 | f"RGB: **RGB{rgb}**", 176 | f"RGBA: **RGBA{rgba}**", 177 | f"Grayscale: **{grayscale}**", 178 | ] 179 | ), 180 | color=0xBEBEFE, 181 | ) 182 | 183 | img = Image.new("RGB", (100, 100), rgb) 184 | 185 | with io.BytesIO() as image_binary: 186 | img.save(image_binary, "PNG") 187 | image_binary.seek(0) 188 | 189 | file = discord.File(fp=image_binary, filename="color.png") 190 | embed.set_image(url="attachment://color.png") 191 | 192 | await context.send(embed=embed, file=file) 193 | except ValueError: 194 | await context.send("Invalid color") 195 | return 196 | 197 | async def setup(bot) -> None: 198 | await bot.add_cog(Utility(bot)) 199 | -------------------------------------------------------------------------------- /cogs/stats.py: -------------------------------------------------------------------------------- 1 | # This project is licensed under the terms of the GPL v3.0 license. Copyright 2024 Cyteon 2 | 3 | import discord 4 | import os 5 | from discord.ext import commands 6 | from discord.ext.commands import Context 7 | from datetime import datetime 8 | 9 | from easy_pil import Font 10 | 11 | from PIL import Image, ImageDraw 12 | from pickledb import PickleDB 13 | 14 | from utils import Checks 15 | 16 | db = PickleDB('pickle/charts.db') 17 | 18 | def textangle(draw, text, xy, angle, fill, font): 19 | img = Image.new('RGBA', font.getsize(text)) 20 | d = ImageDraw.Draw(img) 21 | d.text((0, 0), text, font=font, fill=fill) 22 | w = img.rotate(angle, expand=1) 23 | draw.bitmap(xy, w, fill=fill) 24 | 25 | class Stats(commands.Cog, name="📈 Stats"): 26 | def __init__(self, bot) -> None: 27 | self.bot = bot 28 | 29 | os.makedirs("graphs", exist_ok=True) 30 | 31 | @commands.Cog.listener() 32 | async def on_message(self, message: discord.Message): 33 | if message.guild == None: 34 | return 35 | 36 | if message.author == self.bot.user: 37 | return 38 | 39 | guild_id = str(message.guild.id) 40 | current_date = datetime.utcnow().date() 41 | 42 | if not db.get(guild_id): 43 | db.set(guild_id, {}) 44 | 45 | if str(current_date) not in db.get(guild_id): 46 | db.get(guild_id)[str(current_date)] = {"messages": 0} 47 | 48 | db.get(guild_id)[str(current_date)]["messages"] += 1 49 | 50 | current_users = message.guild.member_count 51 | 52 | db.get(guild_id)[str(current_date)]["users"] = current_users 53 | 54 | guild_data = db.get(guild_id) 55 | for date_str in list(guild_data.keys()): 56 | date = datetime.fromisoformat(date_str).date() 57 | if (current_date - date).days > 30: 58 | del guild_data[date_str] 59 | 60 | db.save() 61 | 62 | @commands.hybrid_group( 63 | name="chart", 64 | description="Show chart of ... activity", 65 | usage="chart" 66 | ) 67 | @commands.check(Checks.is_not_blacklisted) 68 | @commands.check(Checks.command_not_disabled) 69 | async def chart(self, context: Context) -> None: 70 | subcommands = [cmd for cmd in self.chart.walk_commands()] 71 | 72 | data = [] 73 | 74 | for subcommand in subcommands: 75 | description = subcommand.description.partition("\n")[0] 76 | data.append(f"{await self.bot.get_prefix(context)}chart {subcommand.name} - {description}") 77 | 78 | help_text = "\n".join(data) 79 | embed = discord.Embed( 80 | title=f"Help: Chart", description="List of available commands:", color=0xBEBEFE 81 | ) 82 | embed.add_field( 83 | name="Commands", value=f"```{help_text}```", inline=False 84 | ) 85 | 86 | await context.send(embed=embed) 87 | 88 | @chart.command( 89 | name="messages", 90 | description="Show chart of message activity", 91 | usage="chart messages" 92 | ) 93 | @commands.check(Checks.is_not_blacklisted) 94 | @commands.check(Checks.command_not_disabled) 95 | async def messages(self, context: Context) -> None: 96 | guild_id = str(context.guild.id) 97 | if not db.get(guild_id): 98 | await context.send("No data available for this server.") 99 | return 100 | 101 | guild_data = db.get(guild_id) 102 | dates = sorted([datetime.fromisoformat(date_str).date() for date_str in guild_data.keys()]) 103 | message_counts = [guild_data[str(date)]["messages"] for date in dates] 104 | 105 | total_messages = sum(message_counts) 106 | 107 | img = Image.new('RGB', (1600, 800), color=(32, 34, 38)) 108 | draw = ImageDraw.Draw(img) 109 | font = Font.poppins(size=20) 110 | 111 | max_count = max(message_counts) if message_counts else 1 112 | max_count = max_count if max_count != 0 else 1 # Ensure max_count is never zero 113 | y_step = 100 114 | for y in range(100, 701, y_step): 115 | draw.line((100, y, 1500, y), fill=(64, 68, 75), width=2) 116 | number = (max_count - (y - 100) * max_count // 600) / 60 * 60 117 | number = int(str(number).split('.')[0]) 118 | draw.text((20, y - 10), str(number), font=font, fill=(255, 255, 255)) 119 | 120 | bar_color = (128, 128, 128) 121 | line_color = (0, 255, 255) 122 | previous_point = None 123 | 124 | for i in range(len(dates)): 125 | x = 200 + i * 40 126 | y = 700 - (message_counts[i] * 600 // max_count) 127 | draw.line((x, 700, x, y), fill=bar_color, width=20) 128 | if previous_point: 129 | draw.line((previous_point, (x, y)), fill=line_color, width=5) 130 | draw.ellipse((x - 5, y - 5, x + 5, y + 5), fill=line_color) 131 | previous_point = (x, y) 132 | textangle(draw, dates[i].strftime('%d %b'), (x - 40, 720), 45, (255, 255, 255), font) 133 | 134 | label_text = f'Messages - Last 30 days' 135 | total_text = f'Total Messages: {total_messages}' 136 | label_width, _ = draw.textsize(label_text, font=font) 137 | total_width, _ = draw.textsize(total_text, font=font) 138 | draw.text((20, 20), label_text, font=font, fill=(255, 255, 255)) 139 | draw.text((1600 - total_width - 20, 20), total_text, font=font, fill=(255, 255, 255)) 140 | 141 | img.save(f'graphs/graph-msgs-{context.channel.id}.png') 142 | await context.send(file=discord.File(f'graphs/graph-msgs-{context.channel.id}.png')) 143 | 144 | os.remove(f'graphs/graph-msgs-{context.channel.id}.png') 145 | 146 | @chart.command( 147 | name="members", 148 | description="Show chart of member count", 149 | usage="chart members" 150 | ) 151 | @commands.check(Checks.is_not_blacklisted) 152 | @commands.check(Checks.command_not_disabled) 153 | async def members(self, context: Context) -> None: 154 | guild_id = str(context.guild.id) 155 | if not db.get(guild_id): 156 | await context.send("No data available for this server.") 157 | return 158 | 159 | guild_data = db.get(guild_id) 160 | dates = sorted([datetime.fromisoformat(date_str).date() for date_str in guild_data.keys()]) 161 | user_counts = [((guild_data[str(date)]["users"]) if "users" in guild_data[str(date)] else 0) for date in dates] 162 | 163 | img = Image.new('RGB', (1600, 800), color=(32, 34, 38)) 164 | draw = ImageDraw.Draw(img) 165 | font = Font.poppins(size=20) 166 | 167 | max_count = max(user_counts) if user_counts else 1 168 | max_count = max_count if max_count != 0 else 1 169 | 170 | y_step = 100 171 | 172 | for y in range(100, 701, y_step): 173 | draw.line((100, y, 1500, y), fill=(64, 68, 75), width=2) 174 | number = (max_count - (y - 100) * max_count // 600) / 60 * 60 175 | number = int(str(number).split('.')[0]) 176 | draw.text((20, y - 10), str(number), font=font, fill=(255, 255, 255)) 177 | 178 | bar_color = (128, 128, 128) 179 | line_color = (0, 255, 255) 180 | previous_point = None 181 | 182 | for i in range(len(dates)): 183 | x = 200 + i * 40 184 | y = 700 - (user_counts[i] * 600 // max_count) 185 | draw.line((x, 700, x, y), fill=bar_color, width=20) 186 | if previous_point: 187 | draw.line((previous_point, (x, y)), fill=line_color, width=5) 188 | draw.ellipse((x - 5, y - 5, x + 5, y + 5), fill=line_color) 189 | previous_point = (x, y) 190 | textangle(draw, dates[i].strftime('%d %b'), (x - 40, 720), 45, (255, 255, 255), font) 191 | 192 | label_text = f'Member Count - Last 30 days' 193 | 194 | label_width, _ = draw.textsize(label_text, font=font) 195 | draw.text((20, 20), label_text, font=font, fill=(255, 255, 255)) 196 | 197 | img.save(f'graphs/graph-members-{context.channel.id}.png') 198 | await context.send(file=discord.File(f'graphs/graph-members-{context.channel.id}.png')) 199 | 200 | os.remove(f'graphs/graph-members-{context.channel.id}.png') 201 | 202 | async def setup(bot) -> None: 203 | await bot.add_cog(Stats(bot)) 204 | -------------------------------------------------------------------------------- /ui/ticket.py: -------------------------------------------------------------------------------- 1 | import discord 2 | import asyncio 3 | import os 4 | from datetime import datetime 5 | 6 | from discord.ui import Button, button, View 7 | 8 | from utils import ServerLogger, DBClient 9 | 10 | db = DBClient.db 11 | 12 | class CreateButton(View): 13 | def __init__(self): 14 | super().__init__(timeout=None) 15 | 16 | @button(label="Create Ticket",style=discord.ButtonStyle.blurple, emoji="🎫",custom_id="ticketopen") 17 | async def ticket(self, interaction: discord.Interaction, button: Button): 18 | c = db["guilds"] 19 | 20 | data = c.find_one({"id": interaction.guild.id}) 21 | 22 | if not data: 23 | await interaction.channel.send("**Tickets info not found! If you are an admin use `/setting` for more info**") 24 | return 25 | 26 | if not data["tickets_category"] or not data["tickets_support_role"]: 27 | await interaction.channel.send("**Tickets info not found! If you are an admin use `/setting` for more info**") 28 | return 29 | 30 | 31 | await interaction.response.defer(ephemeral=True) 32 | category: discord.CategoryChannel = discord.utils.get(interaction.guild.categories, id=data["tickets_category"]) 33 | for ch in category.text_channels: 34 | if ch.topic == f"{interaction.user.id} DO NOT CHANGE THE TOPIC OF THIS CHANNEL!": 35 | await interaction.followup.send("You already have a ticket in {0}".format(ch.mention), ephemeral=True) 36 | return 37 | 38 | r1 : discord.Role = interaction.guild.get_role(data["tickets_support_role"]) 39 | overwrites = { 40 | interaction.guild.default_role: discord.PermissionOverwrite(read_messages=False), 41 | r1: discord.PermissionOverwrite(read_messages=True, send_messages=True, manage_messages=True), 42 | interaction.user: discord.PermissionOverwrite(read_messages = True, send_messages=True), 43 | interaction.guild.me: discord.PermissionOverwrite(read_messages=True, send_messages=True) 44 | } 45 | channel = await category.create_text_channel( 46 | name=str(interaction.user), 47 | topic=f"{interaction.user.id} DO NOT CHANGE THE TOPIC OF THIS CHANNEL!", 48 | overwrites=overwrites 49 | ) 50 | await channel.send("{0} a ticket has been created!".format(r1.mention)) 51 | await channel.send( 52 | embed=discord.Embed( 53 | title=f"Ticket Created!", 54 | description="Don't ping a staff member, they will be here soon.", 55 | color = discord.Color.green() 56 | ), 57 | view = CloseButton() 58 | ) 59 | await channel.send("Please describe your issue") 60 | 61 | await interaction.followup.send( 62 | embed= discord.Embed( 63 | description = "Created your ticket in {0}".format(channel.mention), 64 | color = discord.Color.blurple() 65 | ), 66 | ephemeral=True 67 | ) 68 | 69 | await ServerLogger.send_log( 70 | title="Ticket Created", 71 | description="Created by {0}".format(interaction.user.mention), 72 | color=discord.Color.green(), 73 | guild=interaction.guild, 74 | channel=interaction.channel 75 | ) 76 | 77 | 78 | class CloseButton(View): 79 | def __init__(self): 80 | super().__init__(timeout=None) 81 | 82 | @button(label="Close the ticket",style=discord.ButtonStyle.red,custom_id="closeticket",emoji="🔒") 83 | async def close(self, interaction: discord.Interaction, button: Button): 84 | c = db["guilds"] 85 | 86 | data = c.find_one({"id": interaction.guild.id}) 87 | 88 | if not data: 89 | await interaction.channel.send("**Tickets info not found! If you are an admin use `/setting` for more info**") 90 | return 91 | 92 | if not data["tickets_category"] or not data["tickets_support_role"]: 93 | await interaction.channel.send("**Tickets info not found! If you are an admin use `/setting` for more info**") 94 | return 95 | 96 | await interaction.response.defer(ephemeral=True) 97 | 98 | await interaction.channel.send("Closing this ticket in 3 seconds!") 99 | 100 | await asyncio.sleep(3) 101 | 102 | category: discord.CategoryChannel = discord.utils.get(interaction.guild.categories, id = data["tickets_category"]) 103 | r1 : discord.Role = interaction.guild.get_role(data["tickets_support_role"]) 104 | overwrites = { 105 | interaction.guild.default_role: discord.PermissionOverwrite(read_messages=False), 106 | r1: discord.PermissionOverwrite(read_messages=True, send_messages=True, manage_messages=True), 107 | interaction.guild.me: discord.PermissionOverwrite(read_messages=True, send_messages=True) 108 | } 109 | await interaction.channel.edit(category=category, overwrites=overwrites) 110 | await interaction.channel.send( 111 | embed= discord.Embed( 112 | description="Ticket Closed!", 113 | color = discord.Color.red() 114 | ), 115 | view = TrashButton() 116 | ) 117 | 118 | member = interaction.guild.get_member(int(interaction.channel.topic.split(" ")[0])) 119 | 120 | os.makedirs("logs", exist_ok=True) 121 | log_file = f"logs/{interaction.channel.id}.log" 122 | with open(log_file, "w", encoding="UTF-8") as f: 123 | f.write( 124 | f'Ticket log from: #{interaction.channel} ({interaction.channel.id}) in the guild "{interaction.guild}" ({interaction.guild.id}) at {datetime.now().strftime("%d.%m.%Y %H:%M:%S")}\n' 125 | ) 126 | async for message in interaction.channel.history( 127 | limit=None, oldest_first=True 128 | ): 129 | attachments = [] 130 | for attachment in message.attachments: 131 | attachments.append(attachment.url) 132 | attachments_text = ( 133 | f"[Attached File{'s' if len(attachments) >= 2 else ''}: {', '.join(attachments)}]" 134 | if len(attachments) >= 1 135 | else "" 136 | ) 137 | f.write( 138 | f"{message.created_at.strftime('%d.%m.%Y %H:%M:%S')} {message.author} {message.id}: {message.clean_content} {attachments_text}\n" 139 | ) 140 | 141 | guilds = DBClient.client.potatobot["guilds"] 142 | data = guilds.find_one({"id": interaction.guild.id}) 143 | 144 | if data["log_channel"]: 145 | log_channel = interaction.guild.get_channel(data["log_channel"]) 146 | 147 | if log_channel: 148 | try: 149 | await log_channel.send(file=discord.File(log_file)) 150 | 151 | embed = discord.Embed( 152 | title="Ticket Closed", 153 | description=f"Ticket {interaction.channel.name} closed by {interaction.user.mention}", 154 | color=discord.Color.orange() 155 | ) 156 | 157 | await log_channel.send(embed=embed) 158 | except: 159 | pass 160 | 161 | try: 162 | with open (log_file, "rb") as f: 163 | await member.send(f"Your ticket in {interaction.guild} has been closed. Transcript: ", file=discord.File(f)) 164 | except Exception as e: 165 | await interaction.channel.send( 166 | f"Couldn't send the log file to {member.mention}, " + str(e) 167 | ) 168 | 169 | os.remove(log_file) 170 | 171 | 172 | class TrashButton(View): 173 | def __init__(self): 174 | super().__init__(timeout=None) 175 | 176 | @button(label="Delete the ticket", style=discord.ButtonStyle.red, emoji="🚮", custom_id="trash") 177 | async def trash(self, interaction: discord.Interaction, button: Button): 178 | await interaction.response.defer() 179 | await interaction.channel.send("Deleting the ticket in 3 seconds") 180 | await asyncio.sleep(3) 181 | 182 | await interaction.channel.delete() 183 | await ServerLogger.send_log( 184 | title="Ticket Deleted", 185 | description=f"Deleted by {interaction.user.mention}, ticket: {interaction.channel.name}", 186 | color=discord.Color.red(), 187 | guild=interaction.guild, 188 | channel=interaction.channel 189 | ) 190 | -------------------------------------------------------------------------------- /ui/farm.py: -------------------------------------------------------------------------------- 1 | import discord 2 | import time 3 | 4 | from discord import ui 5 | from discord.ui import button, View, Modal 6 | 7 | from utils import DBClient, CachedDB 8 | 9 | db = DBClient.db 10 | 11 | class FarmModal(Modal, title = "Buy Saplings (5$ per sapling)"): 12 | def __init__(self, message): 13 | super().__init__(timeout = 60) 14 | 15 | self.message = message 16 | 17 | amount = ui.TextInput(label = "Amount of Sapling", placeholder = "Type max to buy for all your money", style=discord.TextStyle.short, min_length = 1, max_length = 50) 18 | 19 | async def on_submit(self, interaction: discord.Interaction) -> None: 20 | users = db["users"] 21 | data = await CachedDB.find_one(users, {"id": interaction.user.id, "guild_id": interaction.guild.id}) 22 | 23 | value = self.amount.value 24 | 25 | if value == "max": 26 | value = data["wallet"] // 5 27 | else: 28 | if not value.isdigit(): 29 | await interaction.response.send_message("Please enter a valid number", ephemeral=True) 30 | return 31 | 32 | price = 5 * int(value) 33 | 34 | if data["wallet"] < price: 35 | await interaction.response.send_message(f"You cant afford {value} sapling(s) for ${price}", ephemeral=True) 36 | return 37 | 38 | data["wallet"] -= price 39 | data["farm"]["saplings"] += int(value) 40 | 41 | new_data = { 42 | "$set": {"farm": data["farm"], "wallet": data["wallet"]} 43 | } 44 | 45 | await CachedDB.update_one(users, {"id": interaction.user.id, "guild_id": interaction.guild.id}, new_data) 46 | 47 | await interaction.response.send_message(f"Bought {value} sapling(s) for ${price}", ephemeral=True) 48 | 49 | c = db["users"] 50 | data = c.find_one({"id": interaction.user.id, "guild_id": interaction.guild.id}) 51 | 52 | farmData = data["farm"] 53 | 54 | embed = discord.Embed( 55 | title="Farm", 56 | description="Buy saplings to farm potatoes", 57 | color=0x77dd77, 58 | ) 59 | 60 | embed.add_field( 61 | name="Saplings", 62 | value=farmData["saplings"], 63 | inline=False, 64 | ) 65 | 66 | embed.add_field( 67 | name="Crops", 68 | value=farmData["crops"], 69 | inline=False, 70 | ) 71 | 72 | embed.add_field( 73 | name="Harvestable", 74 | value=farmData["harvestable"], 75 | inline=False, 76 | ) 77 | 78 | embed.add_field( 79 | name="Ready", 80 | value=f"", 81 | inline=False, 82 | ) 83 | 84 | embed.set_footer(text=f"Wallet: ${data['wallet']}") 85 | 86 | await interaction.message.edit(embed=embed, view=FarmButton(interaction.user.id)) 87 | 88 | class FarmButton(View): 89 | def __init__(self, authorid): 90 | super().__init__(timeout=None) 91 | self.saplings = 0 92 | self.authorid = authorid 93 | 94 | @button(label="Buy Saplings (show menu)", style=discord.ButtonStyle.primary, custom_id="farm",emoji="🌱") 95 | async def farm(self, interaction: discord.Interaction, button: discord.ui.Button): 96 | if interaction.user.id != self.authorid: 97 | return await interaction.response.send_message("You can't farm someone else's farm", ephemeral=True) 98 | 99 | await interaction.response.send_modal(FarmModal(interaction.message)) 100 | 101 | 102 | @button(label="Plant Crops", style=discord.ButtonStyle.primary, custom_id="plant",emoji="🌾", row=1) 103 | async def plant(self, interaction: discord.Interaction, button: discord.ui.Button): 104 | if interaction.user.id != self.authorid: 105 | return await interaction.response.send_message("You can't plant someone else's crops", ephemeral=True) 106 | 107 | c = db["users"] 108 | data = c.find_one({"id": interaction.user.id, "guild_id": interaction.guild.id}) 109 | 110 | 111 | farmData = data["farm"] 112 | 113 | if not farmData["saplings"] > 0: 114 | await interaction.response.send_message("You don't have any saplings to plant", ephemeral=True) 115 | return 116 | 117 | if farmData["crops"] > 0: 118 | await interaction.response.send_message("You already have crops growing", ephemeral=True) 119 | return 120 | 121 | farmData["crops"] = farmData["saplings"] 122 | farmData["ready_in"] = time.time() + 86400 123 | farmData["saplings"] = 0 124 | 125 | newdata = { 126 | "$set": { 127 | "farm.saplings": farmData["saplings"], 128 | "farm.crops": farmData["crops"], 129 | "farm.ready_in": farmData["ready_in"], 130 | } 131 | } 132 | c.update_one( 133 | {"id": interaction.user.id, "guild_id": interaction.guild.id}, newdata 134 | ) 135 | 136 | await interaction.response.send_message("You planted your crops", ephemeral=True) 137 | 138 | farmData = data["farm"] 139 | 140 | embed = discord.Embed( 141 | title="Farm", 142 | description="Buy saplings to farm potatoes", 143 | color=0x77dd77, 144 | ) 145 | 146 | embed.add_field( 147 | name="Saplings", 148 | value=farmData["saplings"], 149 | inline=False, 150 | ) 151 | 152 | embed.add_field( 153 | name="Crops", 154 | value=farmData["crops"], 155 | inline=False, 156 | ) 157 | 158 | embed.add_field( 159 | name="Harvestable", 160 | value=farmData["harvestable"], 161 | inline=False, 162 | ) 163 | 164 | embed.add_field( 165 | name="Ready", 166 | value=f"", 167 | inline=False, 168 | ) 169 | 170 | embed.set_footer(text=f"Wallet: ${data['wallet']}") 171 | 172 | await interaction.message.edit(embed=embed, view=FarmButton(self.authorid)) 173 | 174 | @button(label="Harvest Crops", style=discord.ButtonStyle.primary, custom_id="harvest",emoji="🥔", row=1) 175 | async def harvest(self, interaction: discord.Interaction, button: discord.ui.Button): 176 | if interaction.user.id != self.authorid: 177 | return await interaction.response.send_message("You can't harvest someone else's crops", ephemeral=True) 178 | 179 | c = db["users"] 180 | data = c.find_one({"id": interaction.user.id, "guild_id": interaction.guild.id}) 181 | 182 | farmData = data["farm"] 183 | 184 | if not farmData["harvestable"] > 0: 185 | await interaction.response.send_message("You don't have any crops to harvest", ephemeral=True) 186 | return 187 | 188 | await interaction.response.send_message("You harvested your crops for $" + str(farmData["harvestable"]*10), ephemeral=True) 189 | 190 | data["wallet"] += farmData["harvestable"]*10 191 | 192 | if farmData["ready_in"] < time.time(): 193 | farmData["harvestable"] = 0 194 | farmData["ready_in"] = time.time() + 86400 195 | 196 | newdata = { 197 | "$set": { 198 | "wallet": data["wallet"], 199 | "farm.harvestable": 0, 200 | "farm.ready_in": farmData["ready_in"], 201 | } 202 | } 203 | c.update_one( 204 | {"id": interaction.user.id, "guild_id": interaction.guild.id}, newdata 205 | ) 206 | 207 | 208 | farmData = data["farm"] 209 | 210 | embed = discord.Embed( 211 | title="Farm", 212 | description="Buy saplings to farm potatoes", 213 | color=0x77dd77, 214 | ) 215 | 216 | embed.add_field( 217 | name="Saplings", 218 | value=farmData["saplings"], 219 | inline=False, 220 | ) 221 | 222 | embed.add_field( 223 | name="Crops", 224 | value=farmData["crops"], 225 | inline=False, 226 | ) 227 | 228 | embed.add_field( 229 | name="Harvestable", 230 | value=farmData["harvestable"], 231 | inline=False, 232 | ) 233 | 234 | embed.add_field( 235 | name="Ready", 236 | value=f"", 237 | inline=False, 238 | ) 239 | 240 | embed.set_footer(text=f"Wallet: ${data['wallet']}") 241 | 242 | await interaction.message.edit(embed=embed, view=FarmButton(self.authorid)) 243 | -------------------------------------------------------------------------------- /cogs/level.py: -------------------------------------------------------------------------------- 1 | # This project is licensed under the terms of the GPL v3.0 license. Copyright 2024 Cyteon 2 | 3 | import random 4 | import pymongo 5 | 6 | import discord 7 | from discord.ext import commands 8 | from discord.ext.commands import Context 9 | 10 | from easy_pil import * 11 | 12 | from utils import CONSTANTS, DBClient, Checks, CachedDB 13 | 14 | db = DBClient.db 15 | 16 | class Level(commands.Cog, name="🚀 Level"): 17 | def __init__(self, bot) -> None: 18 | self.bot = bot 19 | 20 | @commands.hybrid_command( 21 | name="level", 22 | description="See yours or someone elses current level and xp", 23 | usage="level [optional: user]" 24 | ) 25 | @commands.check(Checks.is_not_blacklisted) 26 | @commands.check(Checks.command_not_disabled) 27 | @commands.cooldown(3, 10, commands.BucketType.user) 28 | async def level(self, context: Context, user: discord.Member = None) -> None: 29 | if not user: 30 | user = context.author 31 | 32 | c = db["users"] 33 | data = await CachedDB.find_one(c, {"id": user.id, "guild_id": context.guild.id}) 34 | 35 | if data: 36 | xp_for_next_level = CONSTANTS.LEVELS_AND_XP[data["level"] + 1] 37 | 38 | percentage = round(data["xp"] / xp_for_next_level * 100, 1) 39 | 40 | background = Editor(Canvas((900, 300), color="#141414")) 41 | profile_picture = await load_image_async(str(user.avatar.url)) 42 | profile = Editor(profile_picture).resize((150, 150)).circle_image() 43 | 44 | poppins = Font.poppins(size=40) 45 | poppins_small = Font.poppins(size=30) 46 | 47 | card_right_shape = [(600, 0), (750, 300), (900, 300), (900, 0)] 48 | 49 | background.polygon(card_right_shape, color="#FFFFFF") 50 | background.paste(profile, (30, 30)) 51 | 52 | background.rectangle((30, 220), width=650, height=40, color="#FFFFFF", radius=20) 53 | background.bar((30, 220), max_width=650, height=40, percentage=percentage, color="orange", radius=20) 54 | background.text((200, 40), user.name, font=poppins, color="#FFFFFF") 55 | 56 | background.rectangle((200, 100), width=350, height=2, fill="#FFFFFF") 57 | background.text((200, 130), f"Level {data['level']} - {data['xp']}/{xp_for_next_level} XP", font=poppins_small, color="#FFFFFF") 58 | 59 | file = discord.File(fp=background.image_bytes, filename="level_card.png") 60 | await context.send(file=file) 61 | 62 | else: 63 | await context.send("Start chatting to gain a level") 64 | 65 | @commands.hybrid_command( 66 | name="leaderboard", 67 | description="See the top 10 users with the most xp in this server", 68 | aliases=["lb"], 69 | usage="leaderboard" 70 | ) 71 | @commands.check(Checks.is_not_blacklisted) 72 | @commands.check(Checks.command_not_disabled) 73 | @commands.cooldown(1, 10, commands.BucketType.user) 74 | async def leaderboard(self, context: Context) -> None: 75 | c = db["users"] 76 | data = c.find({"guild_id": context.guild.id}).sort([("level", pymongo.DESCENDING), ("xp", pymongo.DESCENDING)]).limit(10) 77 | 78 | embed = discord.Embed( 79 | title="Leaderboard", 80 | description="", 81 | color=discord.Color.gold() 82 | ) 83 | 84 | for index, user in enumerate(data, start=1): 85 | member = context.guild.get_member(user["id"]) 86 | 87 | if member != None: 88 | if not member.bot: 89 | embed.add_field( 90 | name=f"{index}. {member.nick if member.nick else member.display_name if member.display_name else member.name}", 91 | value=f"Level: {user['level']} - XP: {user['xp']}", 92 | inline=False 93 | ) 94 | else: 95 | fetched = await self.bot.fetch_user(user["id"]) 96 | 97 | if fetched == None: 98 | embed.add_field( 99 | name=f"{index}. Unknown User", 100 | value=f"Level: {user['level']} - XP: {user['xp']}", 101 | inline=False 102 | ) 103 | 104 | if not fetched.bot: 105 | embed.add_field( 106 | name=f"{index}. {fetched.name}", 107 | value=f"Level: {user['level']} - XP: {user['xp']}", 108 | inline=False 109 | ) 110 | 111 | await context.send(embed=embed) 112 | 113 | @commands.Cog.listener() 114 | async def on_message(self, message: discord.Message) -> None: 115 | if message.guild == None: 116 | return 117 | 118 | if message.author == self.bot or message.author.bot: 119 | return 120 | 121 | author = message.author 122 | 123 | c = db["users"] 124 | data = await CachedDB.find_one(c, {"id": author.id, "guild_id": message.guild.id}) 125 | 126 | if not data: 127 | data = CONSTANTS.user_data_template(author.id, message.guild.id) 128 | c.insert_one(data) 129 | 130 | if data["level"] >= CONSTANTS.MAX_LEVEL: 131 | return 132 | 133 | if data["xp"] >= CONSTANTS.MAX_XP: 134 | return 135 | 136 | data["xp"] += random.randint(1, 3) 137 | 138 | if data["xp"] >= CONSTANTS.LEVELS_AND_XP[data["level"] + 1]: 139 | guilds = db["guilds"] 140 | guild_data = await CachedDB.find_one(guilds, {"id": message.guild.id}) 141 | 142 | if not guild_data: 143 | guild_data = CONSTANTS.guild_data_template(message.guild.id) 144 | guilds.insert_one(guild_data) 145 | 146 | data["level"] += 1 147 | data["xp"] = 0 148 | 149 | if str(data["level"]) in guild_data["level_roles"]: 150 | role = message.guild.get_role(guild_data["level_roles"][str(data["level"])]) 151 | await message.author.add_roles(role) 152 | 153 | channel = message.channel 154 | if guild_data: 155 | if "level_announce_channel" in guild_data: 156 | if guild_data["level_announce_channel"] != 0: 157 | channel = message.guild.get_channel(guild_data["level_announce_channel"]) 158 | 159 | if "should_announce_levelup" in guild_data: 160 | if guild_data["should_announce_levelup"]: 161 | await channel.send(f"{author.mention} leveled up to level {data['level']}!") 162 | else: 163 | await channel.send(f"{author.mention} leveled up to level {data['level']}!") 164 | 165 | newdata = {"$set": {"xp": data["xp"], "level": data["level"]}} 166 | 167 | await CachedDB.update_one(c, {"id": author.id, "guild_id": message.guild.id}, newdata) 168 | 169 | @commands.hybrid_command( 170 | name="create-level-roles", 171 | description="Create roles for levels (manage_roles permission)", 172 | usage="create-level-roles" 173 | ) 174 | @commands.check(Checks.is_not_blacklisted) 175 | @commands.check(Checks.command_not_disabled) 176 | @commands.has_permissions(manage_roles=True) 177 | async def create_level_roles(self, context: Context): 178 | # create role for level 1/3/5/10/15/20 179 | 180 | guilds = db["guilds"] 181 | guild_data = await CachedDB.find_one(guilds, {"id": context.guild.id}) 182 | 183 | if not guild_data: 184 | guild_data = CONSTANTS.guild_data_template(context.guild.id) 185 | guilds.insert_one(guild_data) 186 | 187 | for level in [1, 3, 5, 10, 15, 20]: 188 | if str(level) not in guild_data["level_roles"]: 189 | role = await context.guild.create_role(name=f"Level {level}") 190 | guild_data["level_roles"][str(level)] = role.id 191 | 192 | newdata = {"$set": {"level_roles": guild_data["level_roles"]}} 193 | 194 | await CachedDB.update_one(guilds, {"id": context.guild.id}, newdata) 195 | await context.send("Roles created!") 196 | 197 | @commands.hybrid_command( 198 | name="delete-level-roles", 199 | description="Delete roles for levels", 200 | usage="delete-level-roles" 201 | ) 202 | @commands.check(Checks.is_not_blacklisted) 203 | @commands.check(Checks.command_not_disabled) 204 | @commands.has_permissions(manage_roles=True) 205 | async def delete_level_roles(self, context: Context): 206 | guilds = db["guilds"] 207 | guild_data = await CachedDB.find_one(guilds, {"id": context.guild.id}) 208 | 209 | if not guild_data: 210 | guild_data = CONSTANTS.guild_data_template(context.guild.id) 211 | guilds.insert_one(guild_data) 212 | 213 | for level in guild_data["level_roles"]: 214 | role = context.guild.get_role(guild_data["level_roles"][level]) 215 | if role: 216 | try: 217 | await role.delete() 218 | except: 219 | pass 220 | 221 | newdata = {"$set": {"level_roles": {}}} 222 | 223 | await CachedDB.update_one(guilds, {"id": context.guild.id}, newdata) 224 | await context.send("Roles deleted!") 225 | 226 | async def setup(bot) -> None: 227 | await bot.add_cog(Level(bot)) 228 | -------------------------------------------------------------------------------- /cogs/starboard.py: -------------------------------------------------------------------------------- 1 | # This project is licensed under the terms of the GPL v3.0 license. Copyright 2024 Cyteon 2 | 3 | import discord 4 | from discord.ext import commands 5 | from discord.ext.commands import Context 6 | from utils import CONSTANTS, DBClient, Checks, CachedDB 7 | 8 | from ui.starboard import JumpToMessageView 9 | 10 | client = DBClient.client 11 | db = client.potatobot 12 | 13 | class Starboard(commands.Cog, name="⭐ Starboard"): 14 | def __init__(self, bot) -> None: 15 | self.bot = bot 16 | 17 | @commands.Cog.listener() 18 | async def on_raw_reaction_add(self, payload) -> None: 19 | channel = self.bot.get_channel(payload.channel_id) 20 | message = await channel.fetch_message(payload.message_id) 21 | 22 | if message.author.bot: 23 | return 24 | 25 | col = db["guilds"] 26 | guild = await CachedDB.find_one(col, {"id": payload.guild_id}) 27 | 28 | if not guild: 29 | return 30 | 31 | if "starboard" not in guild: 32 | return 33 | 34 | if guild["starboard"]["channel"] == 0: 35 | return 36 | 37 | if "enabled" in guild["starboard"]: 38 | if not guild["starboard"]["enabled"]: 39 | return 40 | 41 | starboard_col = db["starboard"] 42 | starboard_message = await CachedDB.find_one(starboard_col, {"message_id": message.id}) 43 | starboard_channel = self.bot.get_channel(guild["starboard"]["channel"]) 44 | 45 | star_reactions = 0 46 | for r in message.reactions: 47 | if r.emoji == "⭐": 48 | star_reactions = r.count 49 | 50 | if payload.emoji.name == "⭐": 51 | if star_reactions >= guild["starboard"]["threshold"]: 52 | if not starboard_message: 53 | embed = discord.Embed( 54 | description=message.content, 55 | color=0xFFD700, 56 | timestamp=message.created_at 57 | ) 58 | 59 | embed.set_author(name=message.author, icon_url=message.author.display_avatar.url) 60 | 61 | if message.attachments: 62 | embed.set_image(url=message.attachments[0].url) 63 | 64 | label = "⭐ " + str(star_reactions) 65 | 66 | msg = await starboard_channel.send(label, embed=embed, view=JumpToMessageView(message)) 67 | 68 | newdata = { 69 | "message_id": message.id, 70 | "starboard_id": msg.id 71 | } 72 | 73 | starboard_col.insert_one(newdata) 74 | else: 75 | embed = discord.Embed( 76 | description=message.content, 77 | color=0xFFD700, 78 | timestamp=message.created_at 79 | ) 80 | 81 | embed.set_author(name=message.author, icon_url=message.author.display_avatar.url) 82 | 83 | if message.attachments: 84 | embed.set_image(url=message.attachments[0].url) 85 | 86 | label = "⭐ " + str(star_reactions) 87 | 88 | starboard_message = await starboard_channel.fetch_message(starboard_message["starboard_id"]) 89 | 90 | await starboard_message.edit(content=label, embed=embed, view=JumpToMessageView(message)) 91 | 92 | @commands.Cog.listener() 93 | async def on_raw_reaction_remove(self, payload) -> None: 94 | channel = self.bot.get_channel(payload.channel_id) 95 | message = await channel.fetch_message(payload.message_id) 96 | 97 | if message.author.bot: 98 | return 99 | 100 | col = db["guilds"] 101 | guild = await CachedDB.find_one(col, {"id": payload.guild_id}) 102 | 103 | if not guild: 104 | return 105 | 106 | if "starboard" not in guild: 107 | return 108 | 109 | if guild["starboard"]["channel"] == 0: 110 | return 111 | 112 | if "enabled" in guild["starboard"]: 113 | if not guild["starboard"]["enabled"]: 114 | return 115 | 116 | star_reactions = 0 117 | for r in message.reactions: 118 | if r.emoji == "⭐": 119 | star_reactions = r.count 120 | 121 | starboard_col = db["starboard"] 122 | starboard_message = await CachedDB.find_one(starboard_col, {"message_id": message.id}) 123 | starboard_channel = self.bot.get_channel(guild["starboard"]["channel"]) 124 | 125 | if not starboard_channel: 126 | return 127 | 128 | if payload.emoji.name == "⭐": 129 | embed = discord.Embed( 130 | description=message.content, 131 | color=0xFFD700, 132 | timestamp=message.created_at 133 | ) 134 | 135 | embed.set_author(name=message.author, icon_url=message.author.display_avatar.url) 136 | 137 | label = "⭐ " + str(star_reactions) 138 | 139 | if message.attachments: 140 | embed.set_image(url=message.attachments[0].url) 141 | 142 | starboard_message = await starboard_channel.fetch_message(starboard_message["starboard_id"]) 143 | 144 | await starboard_message.edit(content=label, embed=embed, view=JumpToMessageView(message)) 145 | 146 | @commands.hybrid_group( 147 | name="starboard", 148 | description="Commands for managing the starboard.", 149 | usage="starboard " 150 | ) 151 | @commands.check(Checks.is_not_blacklisted) 152 | @commands.check(Checks.command_not_disabled) 153 | async def starboard(self, context: Context) -> None: 154 | subcommands = [cmd for cmd in self.starboard.walk_commands()] 155 | 156 | data = [] 157 | 158 | for subcommand in subcommands: 159 | print(subcommand) 160 | description = subcommand.description.partition("\n")[0] 161 | data.append(f"{await self.bot.get_prefix(context)}starboard {subcommand.name} - {description}") 162 | 163 | help_text = "\n".join(data) 164 | embed = discord.Embed( 165 | title=f"Help: Starboard", description="List of available commands:", color=0xBEBEFE 166 | ) 167 | embed.add_field( 168 | name="Commands", value=f"```{help_text}```", inline=False 169 | ) 170 | 171 | await context.send(embed=embed) 172 | 173 | @starboard.command( 174 | name="channel", 175 | description="Set the starboard channel.", 176 | usage="starboard channel " 177 | ) 178 | @commands.check(Checks.is_not_blacklisted) 179 | @commands.check(Checks.command_not_disabled) 180 | @commands.has_permissions(manage_channels=True) 181 | async def set_starboard(self, context: Context, channel: discord.TextChannel) -> None: 182 | col = db["guilds"] 183 | guild = await CachedDB.find_one(col, {"id": context.guild.id}) 184 | 185 | if not guild: 186 | guild = CONSTANTS.guild_data_template(context.guild.id) 187 | await col.insert_one(CONSTANTS.guild_data_template(context.guild.id)) 188 | 189 | newdata = { 190 | "$set": { 191 | "starboard.channel": channel.id 192 | } 193 | } 194 | 195 | await CachedDB.update_one(col, {"id": context.guild.id}, newdata) 196 | await context.send(f"Starboard channel set to {channel.mention}.") 197 | 198 | @starboard.command( 199 | name="threshold", 200 | description="Set the starboard threshold required to show in starboard channel", 201 | usage="starboard threshold " 202 | ) 203 | @commands.check(Checks.is_not_blacklisted) 204 | @commands.check(Checks.command_not_disabled) 205 | @commands.has_permissions(manage_channels=True) 206 | async def set_threshold(self, context: Context, threshold: int) -> None: 207 | col = db["guilds"] 208 | guild = await CachedDB.find_one(col, {"id": context.guild.id}) 209 | 210 | if not guild: 211 | guild = CONSTANTS.guild_data_template(context.guild.id) 212 | await col.insert_one(guild) 213 | 214 | newdata = { 215 | "$set": { 216 | "starboard.threshold": threshold 217 | } 218 | } 219 | 220 | await CachedDB.update_one(col, {"id": context.guild.id}, newdata) 221 | await context.send(f"Starboard threshold set to {threshold}.") 222 | 223 | @starboard.command( 224 | name="disable", 225 | description="Disable the starboard.", 226 | usage="starboard disable" 227 | ) 228 | @commands.check(Checks.is_not_blacklisted) 229 | @commands.check(Checks.command_not_disabled) 230 | @commands.has_permissions(manage_channels=True) 231 | async def disable_starboard(self, context: Context) -> None: 232 | col = db["guilds"] 233 | guild = await CachedDB.find_one(col, {"id": context.guild.id}) 234 | 235 | if not guild: 236 | guild = CONSTANTS.guild_data_template(context.guild.id) 237 | await col.insert_one(guild) 238 | 239 | newdata = { 240 | "$set": { 241 | "starboard.enabled": False 242 | } 243 | } 244 | 245 | await CachedDB.update_one(col, {"id": context.guild.id}, newdata) 246 | await context.send("Starboard disabled.") 247 | 248 | @starboard.command( 249 | name="enable", 250 | description="Enable the starboard.", 251 | usage="starboard enable" 252 | ) 253 | @commands.check(Checks.is_not_blacklisted) 254 | @commands.check(Checks.command_not_disabled) 255 | 256 | @commands.has_permissions(manage_channels=True) 257 | async def enable_starboard(self, context: Context) -> None: 258 | col = db["guilds"] 259 | guild = await CachedDB.find_one(col, {"id": context.guild.id}) 260 | 261 | if not guild: 262 | guild = CONSTANTS.guild_data_template(context.guild.id) 263 | await col.insert_one(guild) 264 | 265 | newdata = { 266 | "$set": { 267 | "starboard.enabled": True 268 | } 269 | } 270 | 271 | await CachedDB.update_one(col, {"id": context.guild.id}, newdata) 272 | await context.send("Starboard enabled.") 273 | 274 | async def setup(bot) -> None: 275 | await bot.add_cog(Starboard(bot)) 276 | -------------------------------------------------------------------------------- /ui/papertrading.py: -------------------------------------------------------------------------------- 1 | import discord 2 | import asyncio 3 | import aiohttp 4 | import json 5 | import os 6 | from datetime import datetime 7 | from discord import ui 8 | from discord.ui import Button, button, View 9 | 10 | from utils import DBClient 11 | 12 | db = DBClient.db 13 | 14 | # Configuration Constants 15 | # TODO: put this in a config file 16 | ALPHA_VANTAGE_API_KEY = os.getenv('ALPHA_VANTAGE_API_KEY') 17 | MIN_TRADE_AMOUNT = 1 18 | MAX_TRADE_AMOUNT = 100000 19 | TRADE_COOLDOWN = 5 20 | DEMO_MODE = False 21 | MOCK_PRICES = { 22 | "AAPL": 175.50, 23 | "GOOGL": 140.25, 24 | "MSFT": 380.75, 25 | "AMZN": 145.30, 26 | "TSLA": 240.45, 27 | "META": 485.60, 28 | "NVDA": 820.30, 29 | "AMD": 175.25 30 | } 31 | 32 | class StockPortfolioView(View): 33 | def __init__(self, authorid): 34 | super().__init__(timeout=None) 35 | self.authorid = authorid 36 | self.last_trade_time = {} 37 | 38 | @button(label="Buy Stocks", style=discord.ButtonStyle.primary, custom_id="buy_stocks", emoji="📈") 39 | async def buy_stocks(self, interaction: discord.Interaction, button: discord.ui.Button): 40 | if interaction.user.id != self.authorid: 41 | return await interaction.response.send_message("This isn't your trading session!", ephemeral=True) 42 | 43 | current_time = datetime.now().timestamp() 44 | if self.authorid in self.last_trade_time: 45 | time_diff = current_time - self.last_trade_time[self.authorid] 46 | if time_diff < TRADE_COOLDOWN: 47 | return await interaction.response.send_message( 48 | f"Please wait {TRADE_COOLDOWN - int(time_diff)} seconds before trading again!", 49 | ephemeral=True 50 | ) 51 | 52 | self.last_trade_time[self.authorid] = current_time 53 | await interaction.response.send_modal(BuyStocksModal(self.authorid)) 54 | 55 | @button(label="Sell Stocks", style=discord.ButtonStyle.danger, custom_id="sell_stocks", emoji="📉") 56 | async def sell_stocks(self, interaction: discord.Interaction, button: discord.ui.Button): 57 | if interaction.user.id != self.authorid: 58 | return await interaction.response.send_message("This isn't your trading session!", ephemeral=True) 59 | 60 | current_time = datetime.now().timestamp() 61 | if self.authorid in self.last_trade_time: 62 | time_diff = current_time - self.last_trade_time[self.authorid] 63 | if time_diff < TRADE_COOLDOWN: 64 | return await interaction.response.send_message( 65 | f"Please wait {TRADE_COOLDOWN - int(time_diff)} seconds before trading again!", 66 | ephemeral=True 67 | ) 68 | 69 | self.last_trade_time[self.authorid] = current_time 70 | await interaction.response.send_modal(SellStocksModal(self.authorid)) 71 | 72 | @button(label="View Portfolio", style=discord.ButtonStyle.secondary, custom_id="view_portfolio", emoji="📊") 73 | async def view_portfolio(self, interaction: discord.Interaction, button: discord.ui.Button): 74 | if interaction.user.id != self.authorid: 75 | return await interaction.response.send_message("This isn't your trading session!", ephemeral=True) 76 | 77 | c = db["trading"] 78 | portfolio = c.find_one({"user_id": interaction.user.id, "guild_id": interaction.guild.id}) 79 | 80 | if not portfolio or not portfolio.get("positions", {}): 81 | return await interaction.response.send_message("You don't have any positions yet!", ephemeral=True) 82 | 83 | c_users = db["users"] 84 | user = c_users.find_one({"id": interaction.user.id, "guild_id": interaction.guild.id}) 85 | 86 | embed = discord.Embed(title="Your Portfolio", color=0x00ff00) 87 | embed.add_field(name="Available Balance", value=f"${user['wallet']:,.2f}", inline=False) 88 | 89 | total_value = 0 90 | 91 | for symbol, position in portfolio["positions"].items(): 92 | price = await get_stock_price(symbol) 93 | if price: 94 | current_value = position["shares"] * price 95 | total_value += current_value 96 | profit_loss = current_value - (position["shares"] * position["average_price"]) 97 | 98 | embed.add_field( 99 | name=f"{symbol}", 100 | value=f"Shares: {position['shares']}\n" 101 | f"Avg Price: ${position['average_price']:.2f}\n" 102 | f"Current Price: ${price:.2f}\n" 103 | f"P/L: ${profit_loss:.2f} ({(profit_loss/current_value)*100:.1f}%)", 104 | inline=False 105 | ) 106 | 107 | embed.add_field(name="Total Portfolio Value", value=f"${total_value:.2f}", inline=False) 108 | embed.add_field(name="Total Account Value", value=f"${(total_value + user['wallet']):.2f}", inline=False) 109 | await interaction.response.send_message(embed=embed, ephemeral=True) 110 | 111 | class BuyStocksModal(ui.Modal, title="Buy Stocks"): 112 | def __init__(self, authorid): 113 | super().__init__() 114 | self.authorid = authorid 115 | 116 | symbol = ui.TextInput(label="Stock Symbol", placeholder="e.g. AAPL", min_length=1, max_length=5) 117 | shares = ui.TextInput(label="Number of Shares", placeholder="e.g. 10") 118 | 119 | async def on_submit(self, interaction: discord.Interaction): 120 | if interaction.user.id != self.authorid: 121 | return await interaction.response.send_message("This isn't your trading session!", ephemeral=True) 122 | 123 | symbol = self.symbol.value.upper() 124 | try: 125 | shares = float(self.shares.value) 126 | if not MIN_TRADE_AMOUNT <= shares <= MAX_TRADE_AMOUNT: 127 | return await interaction.response.send_message( 128 | f"Please enter between {MIN_TRADE_AMOUNT} and {MAX_TRADE_AMOUNT} shares!", 129 | ephemeral=True 130 | ) 131 | except ValueError: 132 | return await interaction.response.send_message("Please enter a valid number of shares!", ephemeral=True) 133 | 134 | price = await get_stock_price(symbol) 135 | if not price: 136 | return await interaction.response.send_message("Invalid stock symbol or API error!", ephemeral=True) 137 | 138 | total_cost = price * shares 139 | 140 | c = db["users"] 141 | user = c.find_one({"id": interaction.user.id, "guild_id": interaction.guild.id}) 142 | if not user or user["wallet"] < total_cost: 143 | return await interaction.response.send_message( 144 | f"Insufficient funds! You need ${total_cost:,.2f} but have ${user['wallet']:,.2f}", 145 | ephemeral=True 146 | ) 147 | 148 | c = db["trading"] 149 | portfolio = c.find_one({"user_id": interaction.user.id, "guild_id": interaction.guild.id}) 150 | 151 | if not portfolio: 152 | portfolio = { 153 | "user_id": interaction.user.id, 154 | "guild_id": interaction.guild.id, 155 | "positions": {} 156 | } 157 | c.insert_one(portfolio) 158 | 159 | if symbol in portfolio["positions"]: 160 | current_position = portfolio["positions"][symbol] 161 | new_shares = current_position["shares"] + shares 162 | new_average_price = ((current_position["shares"] * current_position["average_price"]) + total_cost) / new_shares 163 | portfolio["positions"][symbol] = { 164 | "shares": new_shares, 165 | "average_price": new_average_price 166 | } 167 | else: 168 | portfolio["positions"][symbol] = { 169 | "shares": shares, 170 | "average_price": price 171 | } 172 | 173 | c.update_one( 174 | {"user_id": interaction.user.id, "guild_id": interaction.guild.id}, 175 | {"$set": {"positions": portfolio["positions"]}} 176 | ) 177 | 178 | user["wallet"] -= total_cost 179 | c = db["users"] 180 | c.update_one( 181 | {"id": interaction.user.id, "guild_id": interaction.guild.id}, 182 | {"$set": {"wallet": user["wallet"]}} 183 | ) 184 | 185 | await interaction.response.send_message( 186 | f"Successfully bought {shares} shares of {symbol} at ${price:.2f} per share.\n" 187 | f"Total cost: ${total_cost:.2f}\n" 188 | f"Remaining balance: ${user['wallet']:,.2f}", 189 | ephemeral=True 190 | ) 191 | 192 | class SellStocksModal(ui.Modal, title="Sell Stocks"): 193 | def __init__(self, authorid): 194 | super().__init__() 195 | self.authorid = authorid 196 | 197 | symbol = ui.TextInput(label="Stock Symbol", placeholder="e.g. AAPL", min_length=1, max_length=5) 198 | shares = ui.TextInput(label="Number of Shares", placeholder="e.g. 10") 199 | 200 | async def on_submit(self, interaction: discord.Interaction): 201 | if interaction.user.id != self.authorid: 202 | return await interaction.response.send_message("This isn't your trading session!", ephemeral=True) 203 | 204 | symbol = self.symbol.value.upper() 205 | try: 206 | shares = float(self.shares.value) 207 | if shares <= 0: 208 | raise ValueError("Shares must be positive") 209 | except ValueError: 210 | return await interaction.response.send_message("Please enter a valid number of shares!", ephemeral=True) 211 | 212 | c = db["trading"] 213 | portfolio = c.find_one({"user_id": interaction.user.id, "guild_id": interaction.guild.id}) 214 | 215 | if not portfolio or symbol not in portfolio["positions"]: 216 | return await interaction.response.send_message("You don't own this stock!", ephemeral=True) 217 | 218 | current_position = portfolio["positions"][symbol] 219 | if current_position["shares"] < shares: 220 | return await interaction.response.send_message( 221 | f"You don't have enough shares! You own {current_position['shares']} shares.", 222 | ephemeral=True 223 | ) 224 | 225 | price = await get_stock_price(symbol) 226 | if not price: 227 | return await interaction.response.send_message("Invalid stock symbol or API error!", ephemeral=True) 228 | 229 | total_value = price * shares 230 | 231 | new_shares = current_position["shares"] - shares 232 | if new_shares == 0: 233 | del portfolio["positions"][symbol] 234 | else: 235 | portfolio["positions"][symbol]["shares"] = new_shares 236 | 237 | c.update_one( 238 | {"user_id": interaction.user.id, "guild_id": interaction.guild.id}, 239 | {"$set": {"positions": portfolio["positions"]}} 240 | ) 241 | 242 | c = db["users"] 243 | user = c.find_one({"id": interaction.user.id, "guild_id": interaction.guild.id}) 244 | user["wallet"] += total_value 245 | c.update_one( 246 | {"id": interaction.user.id, "guild_id": interaction.guild.id}, 247 | {"$set": {"wallet": user["wallet"]}} 248 | ) 249 | 250 | profit_loss = (price - current_position["average_price"]) * shares 251 | 252 | await interaction.response.send_message( 253 | f"Successfully sold {shares} shares of {symbol} at ${price:.2f} per share.\n" 254 | f"Total value: ${total_value:.2f}\n" 255 | f"Profit/Loss: ${profit_loss:.2f}\n" 256 | f"New balance: ${user['wallet']:,.2f}", 257 | ephemeral=True 258 | ) 259 | 260 | async def get_stock_price(symbol): 261 | """Get current stock price using Alpha Vantage API or mock data""" 262 | if DEMO_MODE and symbol in MOCK_PRICES: 263 | return MOCK_PRICES[symbol] 264 | 265 | url = f"https://www.alphavantage.co/query?function=GLOBAL_QUOTE&symbol={symbol}&apikey={ALPHA_VANTAGE_API_KEY}" 266 | 267 | async with aiohttp.ClientSession() as session: 268 | try: 269 | async with session.get(url) as response: 270 | data = await response.json() 271 | if "Global Quote" in data and "05. price" in data["Global Quote"]: 272 | return float(data["Global Quote"]["05. price"]) 273 | return None 274 | except: 275 | return None 276 | 277 | async def start_paper_trading(ctx): 278 | """ 279 | Command to start paper trading session 280 | Usage: !trade or /trade 281 | """ 282 | c = db["users"] 283 | user = c.find_one({"id": ctx.author.id, "guild_id": ctx.guild.id}) 284 | 285 | if not user: 286 | return await ctx.send("get outa here brokie") 287 | 288 | embed = discord.Embed( 289 | title="Paper Trading", 290 | description="Welcome to paper trading! Trade stocks with your existing balance.\n" 291 | "Use the buttons below to buy/sell stocks and view your portfolio.", 292 | color=0x00ff00 293 | ) 294 | embed.add_field( 295 | name="Available Balance", 296 | value=f"${user['wallet']:,.2f}", 297 | inline=False 298 | ) 299 | if DEMO_MODE: 300 | embed.add_field( 301 | name="Available Demo Stocks", 302 | value="\n".join([f"{symbol}: ${price:.2f}" for symbol, price in MOCK_PRICES.items()]), 303 | inline=False 304 | ) 305 | 306 | view = StockPortfolioView(ctx.author.id) 307 | await ctx.send(embed=embed, view=view) 308 | -------------------------------------------------------------------------------- /bot.py: -------------------------------------------------------------------------------- 1 | # pylint: disable-all 2 | 3 | # This project is licensed under the terms of the GPL v3.0 license. Copyright 2024 Cyteon 4 | 5 | import json 6 | import logging 7 | import os 8 | import platform 9 | import random 10 | import sys 11 | import time 12 | import aiohttp 13 | 14 | from pickledb import PickleDB 15 | import pymongo 16 | 17 | import discord 18 | from discord import Webhook 19 | from discord.ext import commands, tasks 20 | 21 | from dotenv import load_dotenv 22 | load_dotenv() 23 | 24 | from utils import ErrorLogger, Errors 25 | 26 | if not os.path.isfile(f"{os.path.realpath(os.path.dirname(__file__))}/config.json"): 27 | sys.exit("'config.json' not found! Please add it and try again.") 28 | else: 29 | with open(f"{os.path.realpath(os.path.dirname(__file__))}/config.json") as file: 30 | config = json.load(file) 31 | 32 | intents = discord.Intents.default() 33 | intents.message_content = True 34 | intents.members = True 35 | 36 | client = pymongo.MongoClient(os.getenv("MONGODB_URL")) 37 | db = client.potatobot 38 | 39 | os.makedirs("pickle", exist_ok=True) 40 | prefixDB = PickleDB("pickle/prefix.db") 41 | statsDB = PickleDB("pickle/stats.db") 42 | 43 | cant_react_in = [] 44 | 45 | class LoggingFormatter(logging.Formatter): 46 | black = "\x1b[30m" 47 | red = "\x1b[31m" 48 | green = "\x1b[32m" 49 | yellow = "\x1b[33m" 50 | blue = "\x1b[34m" 51 | gray = "\x1b[38m" 52 | reset = "\x1b[0m" 53 | bold = "\x1b[1m" 54 | 55 | COLORS = { 56 | logging.DEBUG: gray + bold, 57 | logging.INFO: blue + bold, 58 | logging.WARNING: yellow + bold, 59 | logging.ERROR: red, 60 | logging.CRITICAL: red + bold, 61 | } 62 | 63 | def format(self, record): 64 | log_color = self.COLORS[record.levelno] 65 | format = "(black){asctime}(reset) (levelcolor){levelname:<8}(reset) \x1b[32m{name}(reset) {message}" 66 | format = format.replace("(black)", self.black + self.bold) 67 | format = format.replace("(reset)", self.reset) 68 | format = format.replace("(levelcolor)", log_color) 69 | formatter = logging.Formatter(format, "%Y-%m-%d %H:%M:%S", style="{") 70 | return formatter.format(record) 71 | 72 | logger = logging.getLogger("discord_bot") 73 | logger.setLevel(logging.INFO) 74 | 75 | console_handler = logging.StreamHandler() 76 | console_handler.setFormatter(LoggingFormatter()) 77 | file_handler = logging.FileHandler(filename="discord.log", encoding="utf-8", mode="w") 78 | file_handler_formatter = logging.Formatter( 79 | "[{asctime}] [{levelname:<8}] {name}: {message}", "%Y-%m-%d %H:%M:%S", style="{" 80 | ) 81 | file_handler.setFormatter(file_handler_formatter) 82 | 83 | logger.addHandler(console_handler) 84 | logger.addHandler(file_handler) 85 | 86 | class DiscordBot(commands.AutoShardedBot): 87 | def __init__(self) -> None: 88 | super().__init__( 89 | command_prefix=self.get_prefix, 90 | intents=intents, 91 | help_command=None, 92 | owner_ids=set([int(os.getenv("OWNER_ID"))]), 93 | ) 94 | self.logger = logger 95 | self.config = config 96 | self.version = "2.1.8" 97 | self.start_time = time.time() 98 | self.prefixDB = prefixDB 99 | self.statsDB = statsDB 100 | 101 | async def get_prefix(self, message): 102 | if message.guild: 103 | guild_id = str(message.guild.id) 104 | if prefixDB.get(guild_id): 105 | return prefixDB.get(guild_id) 106 | else: 107 | return config["prefix"] 108 | else: 109 | return config["prefix"] 110 | 111 | async def load_cogs(self) -> None: 112 | for file in os.listdir(f"{os.path.realpath(os.path.dirname(__file__))}/cogs"): 113 | if file.endswith(".py"): 114 | extension = file[:-3] 115 | try: 116 | await self.load_extension(f"cogs.{extension}") 117 | self.logger.info(f"Loaded extension '{extension}'") 118 | except Exception as e: 119 | exception = f"{type(e).__name__}: {e}" 120 | self.logger.error( 121 | f"Failed to load extension {extension}\n{exception}" 122 | ) 123 | 124 | @tasks.loop(minutes=1.0) 125 | async def status_task(self) -> None: 126 | statuses = ["youtube", "netflix"] 127 | await self.change_presence(activity=discord.Activity(type=discord.ActivityType.watching, name=random.choice(statuses))) 128 | 129 | @status_task.before_loop 130 | async def before_status_task(self) -> None: 131 | await self.wait_until_ready() 132 | 133 | 134 | async def setup_hook(self) -> None: 135 | self.logger.info(f"Logged in as {self.user.name}") 136 | self.logger.info(f"discord.py API version: {discord.__version__}") 137 | self.logger.info(f"Python version: {platform.python_version()}") 138 | self.logger.info( 139 | f"Running on: {platform.system()} {platform.release()} ({os.name})" 140 | ) 141 | 142 | self.logger.info("-------------------") 143 | 144 | self.logger.info(f"Connection to db successful: {client.address}") 145 | 146 | self.logger.info("-------------------") 147 | 148 | await self.load_cogs() 149 | 150 | self.logger.info("-------------------") 151 | 152 | self.logger.info(f"Command count (slash+chat): {len([x for x in self.walk_commands() if isinstance(x, commands.HybridCommand)])}") 153 | self.logger.info(f"Command count (chat only): {len([x for x in self.walk_commands() if isinstance(x, commands.Command) and not isinstance(x, commands.HybridCommand)])}") 154 | self.logger.info(f"Total command count: {len([x for x in self.walk_commands()])}") 155 | 156 | self.logger.info( 157 | f"Command groups: {len([x for x in self.walk_commands() if isinstance(x, commands.HybridGroup) or isinstance(x, commands.Group)])}" 158 | ) 159 | self.logger.info(f"Cog count: {len([x for x in self.cogs])}") 160 | 161 | self.logger.info( 162 | f"Discord slash command limit: {len([x for x in self.commands if isinstance(x, commands.HybridCommand) or isinstance(x, commands.HybridGroup)])}/100" 163 | ) 164 | self.logger.info("(Dosent include subcommands)") 165 | 166 | self.logger.info("-------------------") 167 | 168 | self.status_task.start() 169 | 170 | async def on_guild_remove(self, guild: discord.Guild): 171 | async with aiohttp.ClientSession() as session: 172 | to_send = Webhook.from_url(config["join_leave_webhook"], session=session) 173 | 174 | embed = discord.Embed( 175 | title="Bot left a guild!", 176 | description=f"**Guild Name:** {guild.name}\n**Guild ID:** {guild.id}\n**Owner:** {guild.owner.mention if guild.owner else None} ({guild.owner})\n **Member Count:** {guild.member_count}", 177 | color=0xE02B2B 178 | ) 179 | 180 | await to_send.send(embed=embed, username="PotatoBot - Guild Logger") 181 | 182 | self.logger.info("Bot left guild " + guild.name) 183 | 184 | async def on_guild_join(self, guild: discord.Guild): 185 | async with aiohttp.ClientSession() as session: 186 | to_send = Webhook.from_url(config["join_leave_webhook"], session=session) 187 | 188 | embed = discord.Embed( 189 | title="Bot joined a guild!", 190 | description=f"**Guild Name:** {guild.name}\n**Guild ID:** {guild.id}\n**Owner:** {guild.owner.mention if guild.owner else None} ({guild.owner})\n **Member Count:** {guild.member_count}", 191 | color=0x57F287 192 | ) 193 | 194 | await to_send.send(embed=embed, username="PotatoBot - Guild Logger") 195 | 196 | self.logger.info("Bot joined guild: " + guild.name) 197 | 198 | async def on_error(self, event_method, *args, **kwargs): 199 | await ErrorLogger.error(self, event_method, *args, **kwargs) 200 | 201 | async def on_message(self, message: discord.Message) -> None: 202 | if message.author.id in config["fully_ignore"]: 203 | return 204 | 205 | if message.author == self.user or message.author.bot: 206 | return 207 | 208 | arr = message.content.split(" ") 209 | 210 | arr[0] = arr[0].lower() 211 | 212 | message.content = " ".join(arr) 213 | 214 | ctx = await self.get_context(message) 215 | if ctx.command is not None: 216 | self.dispatch('command', ctx) 217 | try: 218 | if await self.can_run(ctx, call_once=True): 219 | await ctx.command.invoke(ctx) 220 | else: 221 | raise commands.errors.CheckFailure('The global check once functions failed.') 222 | except commands.errors.CommandError as exc: 223 | await ctx.command.dispatch_error(ctx, exc) 224 | else: 225 | self.dispatch('command_completion', ctx) 226 | elif ctx.invoked_with: 227 | exc = commands.errors.CommandNotFound(f'Command "{ctx.invoked_with}" is not found') 228 | self.dispatch('command_error', ctx, exc) 229 | else: 230 | if f"<@{str(self.user.id)}>" in message.content: 231 | await message.reply(f"> My prefix is `{await self.get_prefix(message)}`") 232 | 233 | 234 | async def on_command_completion(self, context: commands.Context) -> None: 235 | full_command_name = context.command.qualified_name 236 | split = full_command_name.split(" ") 237 | executed_command = str(split[0]) 238 | 239 | if context.guild is not None: 240 | self.logger.info( 241 | f"Executed {executed_command} command in {context.guild.name} (ID: {context.guild.id}) by {context.author} (ID: {context.author.id})" 242 | ) 243 | else: 244 | self.logger.info( 245 | f"Executed {executed_command} command by {context.author} (ID: {context.author.id}) in DMs" 246 | ) 247 | 248 | commands_ran = (statsDB.get("commands_ran") if statsDB.get("commands_ran") else 0) + 1 249 | statsDB.set("commands_ran", commands_ran) 250 | statsDB.save() 251 | 252 | async def on_command_error(self, context: commands.Context, error) -> None: 253 | if isinstance(error, commands.CommandOnCooldown): 254 | minutes, seconds = divmod(error.retry_after, 60) 255 | hours, minutes = divmod(minutes, 60) 256 | hours = hours % 24 257 | embed = discord.Embed( 258 | 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 ''}.", 259 | color=0xE02B2B, 260 | ) 261 | await context.send(embed=embed) 262 | elif isinstance(error, commands.NotOwner): 263 | embed = discord.Embed( 264 | description="You are not the owner of the bot!", color=0xE02B2B 265 | ) 266 | await context.send(embed=embed) 267 | if context.guild: 268 | self.logger.warning( 269 | 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." 270 | ) 271 | else: 272 | self.logger.warning( 273 | 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." 274 | ) 275 | elif isinstance(error, commands.MissingPermissions): 276 | embed = discord.Embed( 277 | description="You are missing the permission(s) `" 278 | + ", ".join(error.missing_permissions) 279 | + "` to execute this command!", 280 | color=0xE02B2B, 281 | ) 282 | await context.send(embed=embed) 283 | elif isinstance(error, commands.BotMissingPermissions): 284 | embed = discord.Embed( 285 | description="I am missing the permission(s) `" 286 | + ", ".join(error.missing_permissions) 287 | + "` to fully perform this command!", 288 | color=0xE02B2B, 289 | ) 290 | await context.send(embed=embed) 291 | elif isinstance(error, commands.MissingRequiredArgument): 292 | embed = discord.Embed( 293 | title="Error!", 294 | description=str(error).capitalize(), 295 | color=0xE02B2B, 296 | ) 297 | await context.send(embed=embed) 298 | elif isinstance(error, commands.CheckFailure): 299 | embed = discord.Embed( 300 | title="Error!", 301 | description=str(error).capitalize(), 302 | color=0xE02B2B, 303 | ) 304 | await context.send(embed=embed) 305 | elif isinstance(error, commands.CommandNotFound): 306 | if not context.channel in cant_react_in: 307 | try: 308 | await context.message.add_reaction("❓") 309 | except discord.errors.Forbidden: 310 | cant_react_in.append(context.channel) 311 | logger.warning( 312 | f"Couldn't react to a message in {context.channel.name} (ID: {context.channel.id}) in {context.guild.name} (ID: {context.guild.id})" 313 | ) 314 | elif isinstance(error, Errors.CommandDisabled): 315 | embed = discord.Embed( 316 | title="Error!", 317 | description=str(error).capitalize(), 318 | color=0xE02B2B, 319 | ) 320 | await context.send(embed=embed) 321 | elif isinstance(error, Errors.UserBlacklisted): 322 | embed = discord.Embed( 323 | title="Error!", 324 | description=str(error).capitalize(), 325 | color=0xE02B2B, 326 | ) 327 | await context.send(embed=embed) 328 | elif isinstance(error, commands.CommandError): 329 | embed = discord.Embed( 330 | title="Error!", 331 | description=str(error).capitalize(), 332 | color=0xE02B2B, 333 | ) 334 | await context.send(embed=embed) 335 | 336 | await ErrorLogger.command_error(error, context) 337 | else: 338 | if "not found" in str(error): 339 | embed = discord.Embed( 340 | title="Error!", 341 | description=str(error).capitalize(), 342 | color=0xE02B2B, 343 | ) 344 | await context.send(embed=embed) 345 | else: 346 | embed = discord.Embed( 347 | title="Error!", 348 | description=str(error).capitalize(), 349 | color=0xE02B2B, 350 | ) 351 | await context.send(embed=embed) 352 | await ErrorLogger.command_error(error, context) 353 | raise error 354 | -------------------------------------------------------------------------------- /cogs/music.py: -------------------------------------------------------------------------------- 1 | # This project is licensed under the terms of the GPL v3.0 license. Copyright 2024 Cyteon 2 | 3 | import re 4 | import os 5 | import logging 6 | 7 | import discord 8 | import lavalink 9 | from discord.ext import commands 10 | from discord.ext.commands import Context 11 | from lavalink.events import TrackStartEvent, QueueEndEvent 12 | from lavalink.errors import ClientError 13 | from lavalink.filters import LowPass, Timescale 14 | from lavalink.server import LoadType 15 | 16 | url_rx = re.compile(r'https?://(?:www\.)?.+') 17 | 18 | from utils import Checks 19 | 20 | logger = logging.getLogger("discord_bot") 21 | 22 | 23 | host = os.getenv("LAVALINK_HOST") 24 | port = os.getenv("LAVALINK_PORT") 25 | password = os.getenv("LAVALINK_PASSWORD") 26 | region = os.getenv("LAVALINK_REGION") 27 | name = os.getenv("LAVALINK_NAME") 28 | 29 | class LavalinkVoiceClient(discord.VoiceProtocol): 30 | def __init__(self, client: discord.Client, channel: discord.abc.Connectable): 31 | self.client = client 32 | self.channel = channel 33 | self.guild_id = channel.guild.id 34 | self._destroyed = False 35 | 36 | if not hasattr(self.client, 'lavalink'): 37 | self.client.lavalink = lavalink.Client(client.user.id) 38 | self.client.lavalink.add_node(host=host, port=port, password=password, 39 | region=region, name=name) 40 | 41 | self.lavalink = self.client.lavalink 42 | 43 | async def on_voice_server_update(self, data): 44 | lavalink_data = { 45 | 't': 'VOICE_SERVER_UPDATE', 46 | 'd': data 47 | } 48 | await self.lavalink.voice_update_handler(lavalink_data) 49 | 50 | async def on_voice_state_update(self, data): 51 | channel_id = data['channel_id'] 52 | 53 | if not channel_id: 54 | await self._destroy() 55 | return 56 | 57 | self.channel = self.client.get_channel(int(channel_id)) 58 | 59 | lavalink_data = { 60 | 't': 'VOICE_STATE_UPDATE', 61 | 'd': data 62 | } 63 | 64 | await self.lavalink.voice_update_handler(lavalink_data) 65 | 66 | async def connect(self, *, timeout: float, reconnect: bool, self_deaf: bool = False, self_mute: bool = False) -> None: 67 | self.lavalink.player_manager.create(guild_id=self.channel.guild.id) 68 | await self.channel.guild.change_voice_state(channel=self.channel, self_mute=self_mute, self_deaf=self_deaf) 69 | 70 | async def disconnect(self, *, force: bool = False) -> None: 71 | player = self.lavalink.player_manager.get(self.channel.guild.id) 72 | 73 | if not force and not player.is_connected: 74 | return 75 | 76 | await self.channel.guild.change_voice_state(channel=None) 77 | player.channel_id = None 78 | await self._destroy() 79 | 80 | async def _destroy(self): 81 | self.cleanup() 82 | 83 | if self._destroyed: 84 | return 85 | 86 | self._destroyed = True 87 | 88 | try: 89 | await self.lavalink.player_manager.destroy(self.guild_id) 90 | except ClientError: 91 | pass 92 | 93 | 94 | class Music(commands.Cog, name="🎵 Music"): 95 | def __init__(self, bot): 96 | self.bot = bot 97 | 98 | if not hasattr(bot, 'lavalink'): 99 | bot.lavalink = lavalink.Client(bot.user.id) 100 | bot.lavalink.add_node(host=host, port=port, password=password, 101 | region=region, name=name) 102 | 103 | self.lavalink: lavalink.Client = bot.lavalink 104 | self.lavalink.add_event_hooks(self) 105 | 106 | def cog_unload(self): 107 | self.lavalink._event_hooks.clear() 108 | 109 | async def cog_command_error(self, context, error): 110 | if isinstance(error, commands.CommandInvokeError): 111 | await context.send(error.original) 112 | 113 | async def create_player(context: commands.Context): 114 | if context.guild is None: 115 | raise commands.NoPrivateMessage() 116 | 117 | player = context.bot.lavalink.player_manager.create(context.guild.id) 118 | 119 | should_connect = context.command.name in ('play',) 120 | 121 | voice_client = context.voice_client 122 | 123 | if not context.author.voice or not context.author.voice.channel: 124 | if voice_client is not None: 125 | raise commands.CommandInvokeError('You need to join my voice channel first.') 126 | 127 | raise commands.CommandInvokeError('Join a voicechannel first.') 128 | 129 | voice_channel = context.author.voice.channel 130 | 131 | if voice_client is None: 132 | if not should_connect: 133 | raise commands.CommandInvokeError("I'm not playing music.") 134 | 135 | permissions = voice_channel.permissions_for(context.me) 136 | 137 | if not permissions.connect or not permissions.speak: 138 | raise commands.CommandInvokeError('I need the `CONNECT` and `SPEAK` permissions.') 139 | 140 | if voice_channel.user_limit > 0: 141 | if len(voice_channel.members) >= voice_channel.user_limit and not context.me.guild_permissions.move_members: 142 | raise commands.CommandInvokeError('Your voice channel is full!') 143 | 144 | player.store('channel', context.channel.id) 145 | await context.author.voice.channel.connect(cls=LavalinkVoiceClient) 146 | elif voice_client.channel.id != voice_channel.id: 147 | raise commands.CommandInvokeError('You need to be in my voicechannel.') 148 | 149 | return True 150 | 151 | @lavalink.listener(TrackStartEvent) 152 | async def on_track_start(self, event: TrackStartEvent): 153 | guild_id = event.player.guild_id 154 | channel_id = event.player.fetch('channel') 155 | guild = self.bot.get_guild(guild_id) 156 | 157 | if not guild: 158 | return await self.lavalink.player_manager.destroy(guild_id) 159 | 160 | channel = guild.get_channel(channel_id) 161 | 162 | if channel: 163 | await channel.send(f'Now playing: {event.track.title} by {event.track.author}') 164 | logger.info(f"Now playing {event.track.title} in {guild} ({guild.id})") 165 | 166 | 167 | 168 | @lavalink.listener(QueueEndEvent) 169 | async def on_queue_end(self, event: QueueEndEvent): 170 | guild_id = event.player.guild_id 171 | guild = self.bot.get_guild(guild_id) 172 | 173 | if guild is not None: 174 | await guild.voice_client.disconnect(force=True) 175 | 176 | @commands.hybrid_command( 177 | name="play", 178 | description="Searches and plays a song from a given query.", 179 | aliases=['p'], 180 | usage="play " 181 | ) 182 | @commands.check(Checks.is_not_blacklisted) 183 | @commands.check(Checks.command_not_disabled) 184 | @commands.check(create_player) 185 | async def play(self, context, *, query: str): 186 | player = self.bot.lavalink.player_manager.get(context.guild.id) 187 | query = query.strip('<>') 188 | 189 | if not url_rx.match(query): 190 | query = f'ytsearch:{query}' 191 | 192 | results = await player.node.get_tracks(query) 193 | 194 | embed = discord.Embed(color=discord.Color.blurple()) 195 | 196 | if results.load_type == LoadType.EMPTY: 197 | return await context.send("I couldn't find any tracks for that query.") 198 | elif results.load_type == LoadType.PLAYLIST: 199 | tracks = results.tracks 200 | 201 | for track in tracks: 202 | player.add(track=track, requester=context.author.id) 203 | 204 | embed.title = 'Playlist Enqueued!' 205 | embed.description = f'{results.playlist_info.name} - {len(tracks)} tracks' 206 | else: 207 | track = results.tracks[0] 208 | embed.title = 'Track Enqueued' 209 | embed.description = f'[{track.title}]({track.uri})' 210 | 211 | player.add(track=track, requester=context.author.id) 212 | 213 | await context.send(embed=embed) 214 | 215 | if not player.is_playing: 216 | await player.play() 217 | 218 | @commands.hybrid_command( 219 | name="skip", 220 | description="Skip to the next song in the queue", 221 | usage="skip" 222 | ) 223 | @commands.check(Checks.is_not_blacklisted) 224 | @commands.check(Checks.command_not_disabled) 225 | @commands.check(create_player) 226 | async def skip(self, context): 227 | await self.bot.lavalink.player_manager.get(context.guild.id).skip() 228 | 229 | @commands.hybrid_command( 230 | name="pause", 231 | description="Pauses the currently playing track", 232 | usage="pause" 233 | ) 234 | @commands.check(Checks.is_not_blacklisted) 235 | @commands.check(Checks.command_not_disabled) 236 | @commands.check(create_player) 237 | async def pause(self, context): 238 | player = self.bot.lavalink.player_manager.get(context.guild.id) 239 | 240 | if player.is_playing: 241 | await player.set_pause(True) 242 | await context.send('⏸ | Paused the player.') 243 | 244 | @commands.hybrid_command( 245 | name="resume", 246 | description="Resumes the currently paused track", 247 | usage="resume" 248 | ) 249 | @commands.check(Checks.is_not_blacklisted) 250 | @commands.check(Checks.command_not_disabled) 251 | @commands.check(create_player) 252 | async def resume(self, context): 253 | player = self.bot.lavalink.player_manager.get(context.guild.id) 254 | 255 | if player.paused: 256 | await player.set_pause(False) 257 | await context.send('▶ | Resumed the player.') 258 | 259 | @commands.hybrid_command( 260 | name="loop", 261 | description="Enables/disables the loop on the current track", 262 | aliases=['repeat'], 263 | usage="loop" 264 | ) 265 | @commands.check(Checks.is_not_blacklisted) 266 | @commands.check(Checks.command_not_disabled) 267 | @commands.check(create_player) 268 | async def loop(self, context): 269 | player = self.bot.lavalink.player_manager.get(context.guild.id) 270 | player.loop = not player.loop 271 | 272 | await context.send(f"🔁 | {'Enabled' if player.loop else 'Disabled'} loop.") 273 | 274 | @commands.hybrid_group( 275 | name="filter", 276 | description="Commands for managing filters.", 277 | usage="filter " 278 | ) 279 | @commands.check(Checks.is_not_blacklisted) 280 | @commands.check(Checks.command_not_disabled) 281 | async def filter(self, context: Context) -> None: 282 | prefix = await self.bot.get_prefix(context) 283 | 284 | cmds = "\n".join([f"{prefix}filter {cmd.name} - {cmd.description}" for cmd in self.filter.walk_commands()]) 285 | 286 | embed = discord.Embed( 287 | title=f"Help: Filter", description="List of available commands:", color=0xBEBEFE 288 | ) 289 | embed.add_field( 290 | name="Commands", value=f"```{cmds}```", inline=False 291 | ) 292 | 293 | await context.send(embed=embed) 294 | 295 | @filter.command( 296 | name="lowpass", 297 | description="Sets the strength of the low pass filter", 298 | usage="filer lowpass " 299 | ) 300 | @commands.check(Checks.is_not_blacklisted) 301 | @commands.check(Checks.command_not_disabled) 302 | @commands.check(create_player) 303 | async def lowpass(self, context, strength: float = 0.0): 304 | player = self.bot.lavalink.player_manager.get(context.guild.id) 305 | 306 | strength = max(0, strength) 307 | strength = min(1000, strength) 308 | 309 | if strength < 1 and strength != 0.0: 310 | return await context.send('The strength must be greater than 1.') 311 | 312 | embed = discord.Embed(color=discord.Color.blurple(), title='Low Pass Filter') 313 | 314 | if strength == 0.0: 315 | await player.remove_filter('lowpass') 316 | embed.description = 'Disabled **Low Pass Filter**' 317 | return await context.send(embed=embed) 318 | 319 | low_pass = LowPass() 320 | low_pass.update(smoothing=strength) 321 | 322 | await player.set_filter(low_pass) 323 | 324 | embed.description = f'Set **Low Pass Filter** strength to {strength}.' 325 | await context.send(embed=embed) 326 | 327 | 328 | @filter.command( 329 | name="pitch", 330 | description="Sets the player pitch", 331 | aliases=['ptch'], 332 | usage="filter pitch " 333 | ) 334 | @commands.check(Checks.is_not_blacklisted) 335 | @commands.check(Checks.command_not_disabled) 336 | @commands.check(create_player) 337 | async def pitch(self, context: Context, pitch: float): 338 | player = self.bot.lavalink.player_manager.get(context.guild.id) 339 | 340 | pitch = max(0.1, pitch) 341 | 342 | timescale = Timescale() 343 | timescale.pitch = pitch 344 | await player.set_filter(timescale) 345 | 346 | await context.send(f"🎵 | Set the player pitch to {pitch}.") 347 | 348 | 349 | @filter.command( 350 | name="speed", 351 | description="Sets the player speed", 352 | aliases=['spd'], 353 | usage="filter speed " 354 | ) 355 | @commands.check(Checks.is_not_blacklisted) 356 | @commands.check(Checks.command_not_disabled) 357 | @commands.check(create_player) 358 | async def speed(self, context: Context, speed: float): 359 | player = self.bot.lavalink.player_manager.get(context.guild.id) 360 | 361 | speed = max(0.1, speed) 362 | 363 | timescale = Timescale() 364 | timescale.speed = speed 365 | await player.set_filter(timescale) 366 | 367 | await context.send(f"🏃 | Set the player speed to {speed}.") 368 | 369 | @commands.hybrid_command( 370 | name="disconnect", 371 | description="Disconnects the player from the voice channel and clears the queue", 372 | aliases=['dc', 'leave', 'stop'], 373 | usage="disconnect" 374 | ) 375 | @commands.check(Checks.is_not_blacklisted) 376 | @commands.check(Checks.command_not_disabled) 377 | @commands.check(create_player) 378 | async def disconnect(self, context): 379 | player = self.bot.lavalink.player_manager.get(context.guild.id) 380 | 381 | player.queue.clear() 382 | await player.stop() 383 | await context.voice_client.disconnect(force=True) 384 | await context.send('✳ | Disconnected.') 385 | 386 | @commands.hybrid_command( 387 | name="volume", 388 | description="Sets the player volume", 389 | aliases=['vol'], 390 | usage="volume " 391 | ) 392 | @commands.check(Checks.is_not_blacklisted) 393 | @commands.check(Checks.command_not_disabled) 394 | @commands.check(create_player) 395 | async def volume(self, context: Context, volume: int): 396 | player = self.bot.lavalink.player_manager.get(context.guild.id) 397 | 398 | volume = max(1, volume) 399 | volume = min(100, volume) 400 | 401 | await player.set_volume(volume) 402 | await context.send(f"🔊 | Set the player volume to {volume}.") 403 | 404 | 405 | async def setup(bot) -> None: 406 | await bot.add_cog(Music(bot)) 407 | -------------------------------------------------------------------------------- /cogs/economy.py: -------------------------------------------------------------------------------- 1 | # This project is licensed under the terms of the GPL v3.0 license. Copyright 2024 Cyteon 2 | 3 | import random 4 | import discord 5 | import time 6 | 7 | from discord import ui 8 | from discord.ext import commands 9 | from discord.ext.commands import Context 10 | 11 | from utils import CONSTANTS, DBClient, CachedDB, Checks 12 | from ui.farm import FarmButton 13 | from ui.gambling import GamblingButton 14 | 15 | from ui.papertrading import start_paper_trading 16 | 17 | db = DBClient.db 18 | 19 | class Economy(commands.Cog, name="🪙 Economy"): 20 | def __init__(self, bot) -> None: 21 | self.bot = bot 22 | 23 | @commands.hybrid_command( 24 | name="balance", 25 | aliases=["wallet", "bal"], 26 | description="See yours or someone else's wallet", 27 | usage="balance [optional: user]" 28 | ) 29 | @commands.check(Checks.is_not_blacklisted) 30 | @commands.check(Checks.command_not_disabled) 31 | @commands.cooldown(3, 10, commands.BucketType.user) 32 | async def wallet(self, context: Context, user: discord.Member = None) -> None: 33 | if not user: 34 | user = context.author 35 | 36 | c = db["users"] 37 | data = c.find_one({"id": user.id, "guild_id": context.guild.id}) 38 | 39 | if not data: 40 | data = CONSTANTS.user_data_template(user.id, context.guild.id) 41 | c.insert_one(data) 42 | await context.send(f"**{user}** has ${data['wallet']} in their wallet") 43 | 44 | @commands.hybrid_command( 45 | name="daily", 46 | description="Get your daily cash", 47 | usage="daily" 48 | ) 49 | @commands.check(Checks.is_not_blacklisted) 50 | @commands.check(Checks.command_not_disabled) 51 | async def daily(self, context: Context) -> None: 52 | c = db["users"] 53 | data = await CachedDB.find_one(c, {"id": context.author.id, "guild_id": context.guild.id}) 54 | 55 | if not data: 56 | data = CONSTANTS.user_data_template(context.author.id, context.guild.id) 57 | c.insert_one(data) 58 | if time.time() - data["last_daily"] < 86400: 59 | eta = data["last_daily"] + 86400 60 | await context.send( 61 | f"You can claim your daily cash " 62 | ) 63 | return 64 | 65 | guild = db["guilds"] 66 | guild_data = await CachedDB.find_one(guild, {"id": context.guild.id}) 67 | 68 | if not guild_data: 69 | guild_data = CONSTANTS.guild_data_template(context.guild.id) 70 | guild.insert_one(guild_data) 71 | 72 | data["wallet"] += guild_data["daily_cash"] 73 | newdata = { 74 | "$set": {"wallet": data["wallet"], "last_daily": time.time()} 75 | } 76 | 77 | await CachedDB.update_one(c, {"id": context.author.id, "guild_id": context.guild.id}, newdata) 78 | 79 | await context.send(f"Added {guild_data['daily_cash']}$ to wallet") 80 | 81 | @commands.hybrid_command( 82 | name="beg", 83 | description="Beg for money", 84 | usage="beg" 85 | ) 86 | @commands.check(Checks.is_not_blacklisted) 87 | @commands.check(Checks.command_not_disabled) 88 | @commands.cooldown(1, 600, commands.BucketType.user) 89 | async def beg(self, context: Context) -> None: 90 | c = db["users"] 91 | data = await CachedDB.find_one(c, {"id": context.author.id, "guild_id": context.guild.id}) 92 | 93 | if not data: 94 | data = CONSTANTS.user_data_template(context.author.id, context.guild.id) 95 | c.insert_one(data) 96 | 97 | amount = random.randint(5, 200) 98 | data["wallet"] += amount 99 | 100 | newdata = { 101 | "$set": {"wallet": data["wallet"]} 102 | } 103 | 104 | await CachedDB.update_one(c, {"id": context.author.id, "guild_id": context.guild.id}, newdata) 105 | 106 | await context.send(f"Someone gave you {amount}$!") 107 | 108 | @commands.hybrid_command( 109 | name="rob", 110 | description="Rob someone's wallet", 111 | usage="rob " 112 | ) 113 | @commands.check(Checks.is_not_blacklisted) 114 | @commands.check(Checks.command_not_disabled) 115 | @commands.cooldown(1, 3600, commands.BucketType.user) 116 | async def rob(self, context: Context, user: discord.Member) -> None: 117 | if user == context.author: 118 | await context.send("You can't rob yourself") 119 | return 120 | 121 | c = db["users"] 122 | 123 | target_data = await CachedDB.find_one(c, {"id": user.id, "guild_id": context.guild.id}) 124 | 125 | if not target_data: 126 | return await context.send("User has no money") 127 | 128 | if target_data["wallet"] == 0: 129 | return await context.send("User has no money") 130 | 131 | author_data = await CachedDB.find_one(c, {"id": context.author.id, "guild_id": context.guild.id}) 132 | 133 | if not author_data: 134 | author_data = CONSTANTS.user_data_template(context.author.id, context.guild.id) 135 | c.insert_one(author_data) 136 | 137 | max_payout = target_data["wallet"] // 5 138 | 139 | if target_data["last_robbed_at"] > time.time() - 10800: 140 | eta = target_data["last_robbed_at"] + 10800 141 | await context.send( 142 | f"This user can be robbed again " 143 | ) 144 | return 145 | 146 | result = random.randint(0, 2) 147 | if result == 0: 148 | payout = random.randint(1, max_payout) 149 | author_data["wallet"] += payout 150 | target_data["wallet"] -= payout 151 | 152 | newdata = { 153 | "$set": { 154 | "wallet": author_data["wallet"], 155 | } 156 | } 157 | 158 | newdata2 = { 159 | "$set": { 160 | "wallet": target_data["wallet"], 161 | "last_robbed_at": time.time() 162 | } 163 | } 164 | 165 | await CachedDB.update_one(c, {"id": context.author.id, "guild_id": context.guild.id}, newdata) 166 | await CachedDB.update_one(c, {"id": user.id, "guild_id": context.guild.id}, newdata2) 167 | 168 | await context.send(f"You successfully robbed {user} and got {payout}$") 169 | elif result == 1: 170 | payout = min(random.randint(1, max_payout//2), author_data["wallet"]//3, 10000) 171 | author_data["wallet"] -= payout 172 | target_data["wallet"] += payout 173 | 174 | newdata = { 175 | "$set": { 176 | "wallet": author_data["wallet"], 177 | } 178 | } 179 | 180 | newdata2 = { 181 | "$set": {"wallet": target_data["wallet"], "last_robbed_at": time.time()} 182 | } 183 | 184 | await CachedDB.update_one(c, {"id": context.author.id, "guild_id": context.guild.id}, newdata) 185 | await CachedDB.update_one(c, {"id": user.id, "guild_id": context.guild.id}, newdata2) 186 | 187 | await context.send(f"You got caught by {user} and they took {payout}$") 188 | else: 189 | await context.send(f"You failed to rob {user}, but lost nothing") 190 | 191 | @commands.hybrid_command( 192 | name="baltop", 193 | description="See the top 10 richest users", 194 | usage="baltop" 195 | ) 196 | @commands.check(Checks.is_not_blacklisted) 197 | @commands.check(Checks.command_not_disabled) 198 | async def baltop(self, context: Context) -> None: 199 | c = db["users"] 200 | data = c.find({"guild_id": context.guild.id}).sort("wallet", -1).limit(10) 201 | 202 | embed = discord.Embed( 203 | title="Top Balances", 204 | description="", 205 | color=discord.Color.gold(), 206 | ) 207 | 208 | i = 1 209 | for _, user in enumerate(data, start=1): 210 | member = context.guild.get_member(user["id"]) 211 | if member != None: 212 | if member.bot: 213 | continue 214 | embed.add_field( 215 | name=f"{i}. {member.nick if member.nick else member.display_name if member.display_name else member.name}", 216 | value=f"${user['wallet']}", 217 | inline=False, 218 | ) 219 | i += 1 220 | 221 | await context.send(embed=embed) 222 | 223 | @commands.hybrid_command( 224 | name="pay", 225 | description="Pay someone from your wallet", 226 | usage="pay " 227 | ) 228 | @commands.check(Checks.is_not_blacklisted) 229 | @commands.check(Checks.command_not_disabled) 230 | async def pay(self, context: Context, user: discord.Member, amount: int) -> None: 231 | if amount < 0: 232 | await context.send("You can't pay a negative amount") 233 | return 234 | 235 | if user == context.author: 236 | await context.send("You can't pay yourself") 237 | return 238 | 239 | c = db["users"] 240 | data = await CachedDB.find_one(c, {"id": context.author.id, "guild_id": context.guild.id}) 241 | 242 | if not data: 243 | data = CONSTANTS.user_data_template(context.author.id, context.guild.id) 244 | c.insert_one(data) 245 | if data["wallet"] < amount: 246 | await context.send("You don't have enough money") 247 | return 248 | 249 | target_user_data = c.find_one({"id": user.id, "guild_id": context.guild.id}) 250 | if not target_user_data: 251 | target_user_data = CONSTANTS.user_data_template(user.id, context.guild.id) 252 | 253 | c.insert_one(target_user_data) 254 | data["wallet"] -= amount 255 | target_user_data["wallet"] += amount 256 | newdata = { 257 | "$set": {"wallet": data["wallet"]} 258 | } 259 | newdata2 = { 260 | "$set": {"wallet": target_user_data["wallet"]} 261 | } 262 | 263 | await CachedDB.update_one(c, {"id": context.author.id, "guild_id": context.guild.id}, newdata) 264 | await CachedDB.update_one(c, {"id": user.id, "guild_id": context.guild.id}, newdata2) 265 | 266 | await context.send(f"Paid {amount}$ to {user.mention}") 267 | 268 | @commands.hybrid_command( 269 | name="set", 270 | description="Set someones wallet (admin only)", 271 | usage="set " 272 | ) 273 | @commands.check(Checks.is_not_blacklisted) 274 | @commands.check(Checks.command_not_disabled) 275 | @commands.has_permissions(manage_messages=True) 276 | async def set(self, context: Context, user: discord.Member, amount: int) -> None: 277 | c = db["users"] 278 | 279 | target_user_data = await CachedDB.find_one(c, {"id": user.id, "guild_id": context.guild.id}) 280 | 281 | if not target_user_data: 282 | target_user_data = CONSTANTS.user_data_template(context.author.id, context.guild.id) 283 | 284 | c.insert_one(target_user_data) 285 | 286 | newdata = { 287 | "$set": {"wallet": amount} 288 | } 289 | 290 | await CachedDB.update_one(c, {"id": user.id, "guild_id": context.guild.id}, newdata) 291 | 292 | await context.send(f"Set {user.mention}'s wallet to {amount}$") 293 | 294 | @commands.hybrid_command( 295 | name="gamble", 296 | description="Gamble your money", 297 | usage="gamble " 298 | ) 299 | @commands.check(Checks.is_not_blacklisted) 300 | @commands.check(Checks.command_not_disabled) 301 | async def gamble(self, context: Context, amount: int) -> None: 302 | if amount < 0: 303 | await context.send("You can't gamble a negative amount") 304 | return 305 | 306 | c = db["users"] 307 | data = await CachedDB.find_one(c, {"id": context.author.id, "guild_id": context.guild.id}) 308 | 309 | if not data: 310 | data = CONSTANTS.user_data_template(context.author.id, context.guild.id) 311 | c.insert_one(data) 312 | if data["wallet"] < amount: 313 | await context.send("You don't have enough money") 314 | return 315 | 316 | if amount < 1: 317 | await context.send("You can't gamble less than 1$") 318 | return 319 | 320 | await context.send( 321 | "How would you like to gamble?", 322 | view=GamblingButton(amount, context.author.id), 323 | ) 324 | 325 | @commands.hybrid_command( 326 | name="stockmarket", 327 | description="Gamble your money(but like irl but like fake fr)", 328 | usage="stockmarket" 329 | ) 330 | @commands.check(Checks.is_not_blacklisted) 331 | @commands.check(Checks.command_not_disabled) 332 | async def stockmarket(self, context: Context) -> None: 333 | await start_paper_trading(context) 334 | 335 | # TODO: MORE CACHING AFTER THIS POINT 336 | 337 | @commands.hybrid_command( 338 | name="farm", 339 | description="Farm some potatoes", 340 | usage="farm" 341 | ) 342 | @commands.check(Checks.is_not_blacklisted) 343 | @commands.check(Checks.command_not_disabled) 344 | async def farm(self, context: Context) -> None: 345 | 346 | c = db["users"] 347 | data = await CachedDB.find_one(c, {"id": context.author.id, "guild_id": context.guild.id}) 348 | 349 | if not data: 350 | data = CONSTANTS.user_data_template(context.author.id, context.guild.id) 351 | c.insert_one(data) 352 | 353 | if not "farm" in data: 354 | data["farm"] = { 355 | "saplings": 0, 356 | "crops": 0, 357 | "harvestable": 0, 358 | "ready_in": 0 359 | } 360 | newdata = { 361 | "$set": {"farm": data["farm"]} 362 | } 363 | c.update_one( 364 | {"id": context.author.id, "guild_id": context.guild.id}, newdata 365 | ) 366 | 367 | farmData = data["farm"] 368 | 369 | if farmData["ready_in"] < time.time(): 370 | farmData["harvestable"] += farmData["crops"] 371 | farmData["crops"] = 0 372 | 373 | embed = discord.Embed( 374 | title="Farm", 375 | description="Buy saplings to farm potatoes", 376 | color=0x77dd77, 377 | ) 378 | 379 | embed.add_field( 380 | name="Saplings", 381 | value=farmData["saplings"], 382 | inline=False, 383 | ) 384 | 385 | embed.add_field( 386 | name="Crops", 387 | value=farmData["crops"], 388 | inline=False, 389 | ) 390 | 391 | embed.add_field( 392 | name="Harvestable", 393 | value=farmData["harvestable"], 394 | inline=False, 395 | ) 396 | 397 | embed.add_field( 398 | name="Ready", 399 | value=f"", 400 | inline=False, 401 | ) 402 | 403 | embed.set_footer(text=f"Wallet: ${data['wallet']}") 404 | 405 | await context.send(embed=embed, view=FarmButton(context.author.id)) 406 | 407 | new_data = { 408 | "$set": {"farm": farmData} 409 | } 410 | c.update_one( 411 | {"id": context.author.id, "guild_id": context.guild.id}, new_data 412 | ) 413 | 414 | async def setup(bot) -> None: 415 | await bot.add_cog(Economy(bot)) 416 | -------------------------------------------------------------------------------- /cogs/ticket.py: -------------------------------------------------------------------------------- 1 | # This project is licensed under the terms of the GPL v3.0 license. Copyright 2024 Cyteon 2 | 3 | import discord 4 | import os 5 | from datetime import datetime 6 | from discord.ext import commands 7 | from discord.ext.commands import Context 8 | 9 | from utils import ServerLogger, DBClient, Checks 10 | from ui.ticket import CreateButton, CloseButton, TrashButton 11 | 12 | client = DBClient.client 13 | db = client.potatobot 14 | 15 | class Ticket(commands.Cog, name="🎫 Ticket"): 16 | def __init__(self, bot) -> None: 17 | self.bot = bot 18 | 19 | @commands.hybrid_command( 20 | name="ticketembed", 21 | description="Command to make a embed for making tickets", 22 | usage="ticketembed" 23 | ) 24 | @commands.check(Checks.is_not_blacklisted) 25 | @commands.check(Checks.command_not_disabled) 26 | @commands.has_permissions(administrator=True) 27 | async def ticketembed(self, context): 28 | await context.send( 29 | embed = discord.Embed( 30 | description="Press the button to create a new ticket!", 31 | color=discord.Color.blue() 32 | ), 33 | view = CreateButton() 34 | ) 35 | 36 | @commands.hybrid_command( 37 | name="open", 38 | description="Open a ticket", 39 | usage="open" 40 | ) 41 | @commands.check(Checks.is_not_blacklisted) 42 | @commands.check(Checks.command_not_disabled) 43 | async def open(self, context: Context): 44 | c = db["guilds"] 45 | 46 | data = c.find_one({"id": context.guild.id}) 47 | 48 | if not data: 49 | await context.send("**Tickets info not found! If you are an admin use `/setting` for more info**") 50 | return 51 | 52 | if not data["tickets_category"]: 53 | await context.send("**Tickets info not found! If you are an admin use `/setting` for more info**") 54 | return 55 | 56 | category: discord.CategoryChannel = discord.utils.get(context.guild.categories, id=data["tickets_category"]) 57 | for ch in category.text_channels: 58 | if ch.topic == f"{context.author.id} DO NOT CHANGE THE TOPIC OF THIS CHANNEL!": 59 | await context.send("You already have a ticket in {0}".format(ch.mention)) 60 | return 61 | r1 = None 62 | 63 | if data["tickets_support_role"]: 64 | r1 : discord.Role = context.guild.get_role(data["tickets_support_role"]) 65 | else: 66 | r1 = context.guild.default_role 67 | overwrites = { 68 | context.guild.default_role: discord.PermissionOverwrite(read_messages=False), 69 | r1: discord.PermissionOverwrite(read_messages=True, send_messages=True, manage_messages=True), 70 | context.author: discord.PermissionOverwrite(read_messages = True, send_messages=True), 71 | context.guild.me: discord.PermissionOverwrite(read_messages=True, send_messages=True) 72 | } 73 | channel = await category.create_text_channel( 74 | name=str(context.author), 75 | topic=f"{context.author.id} DO NOT CHANGE THE TOPIC OF THIS CHANNEL!", 76 | overwrites=overwrites 77 | ) 78 | await channel.send("{0} a ticket has been created!".format(r1.mention)) 79 | await channel.send( 80 | embed=discord.Embed( 81 | title=f"Ticket Created!", 82 | description="Don't ping a staff member, they will be here soon.", 83 | color = discord.Color.green() 84 | ), 85 | view = CloseButton() 86 | ) 87 | await channel.send("Please describe your issue") 88 | 89 | await context.send( 90 | embed= discord.Embed( 91 | description = "Created your ticket in {0}".format(channel.mention), 92 | color = discord.Color.blurple() 93 | ) 94 | ) 95 | 96 | await ServerLogger.send_log( 97 | title="Ticket Created", 98 | description="Created by {0}".format(context.author.mention), 99 | color=discord.Color.green(), 100 | guild=context.guild, 101 | channel=context.channel 102 | ) 103 | 104 | @commands.hybrid_group( 105 | name="ticket", 106 | description="Commands to manage a ticket", 107 | usage="ticket" 108 | ) 109 | @commands.check(Checks.is_not_blacklisted) 110 | @commands.check(Checks.command_not_disabled) 111 | async def ticket(self, context: Context): 112 | subcommands = [cmd for cmd in self.ticket.walk_commands()] 113 | 114 | data = [] 115 | 116 | for subcommand in subcommands: 117 | description = subcommand.description.partition("\n")[0] 118 | data.append(f"{await self.bot.get_prefix(context)}ticket {subcommand.name} - {description}") 119 | 120 | help_text = "\n".join(data) 121 | embed = discord.Embed( 122 | title=f"Help: Ticket", description="List of available commands:", color=0xBEBEFE 123 | ) 124 | embed.add_field( 125 | name="Commands", value=f"```{help_text}```", inline=False 126 | ) 127 | 128 | await context.send(embed=embed) 129 | 130 | @ticket.command( 131 | name="upgrade", 132 | description="Remove support role access from the ticket", 133 | usage="ticket upgrade" 134 | ) 135 | @commands.check(Checks.is_not_blacklisted) 136 | @commands.check(Checks.command_not_disabled) 137 | @commands.has_permissions(manage_channels=True) 138 | async def upgrade(self, context: Context): 139 | try: 140 | int(context.channel.topic.split()[0]) 141 | except: 142 | return await context.send("This is not a ticket channel.") 143 | 144 | c = db["guilds"] 145 | guild = c.find_one({"id": context.guild.id}) 146 | 147 | if not guild or not guild.get("tickets_support_role"): 148 | return await context.send("Support role not configured for this server.") 149 | 150 | support_role = context.guild.get_role(guild["tickets_support_role"]) 151 | if not support_role: 152 | return await context.send("Support role not found.") 153 | 154 | if support_role not in context.channel.overwrites: 155 | return await context.send("This ticket is already upgraded.") 156 | 157 | await context.channel.set_permissions(support_role, overwrite=None) 158 | await context.send("Support role access has been removed from this ticket.") 159 | 160 | await ServerLogger.send_log( 161 | title="Ticket Upgraded", 162 | description=f"{context.author.mention} upgraded ticket {context.channel.name}", 163 | color=discord.Color.purple(), 164 | guild=context.guild, 165 | channel=context.channel 166 | ) 167 | 168 | @ticket.command(name="downgrade", description="Restore support role access to the ticket", usage="ticket downgrade") 169 | @commands.check(Checks.is_not_blacklisted) 170 | @commands.check(Checks.command_not_disabled) 171 | @commands.has_permissions(manage_channels=True) 172 | async def downgrade(self, context: Context): 173 | try: 174 | int(context.channel.topic.split()[0]) 175 | except: 176 | return await context.send("This is not a ticket channel.") 177 | 178 | c = db["guilds"] 179 | guild = c.find_one({"id": context.guild.id}) 180 | 181 | if not guild or not guild.get("tickets_support_role"): 182 | return await context.send("Support role not configured for this server.") 183 | 184 | support_role = context.guild.get_role(guild["tickets_support_role"]) 185 | if not support_role: 186 | return await context.send("Support role not found.") 187 | 188 | if support_role in context.channel.overwrites: 189 | return await context.send("This ticket is already accessible to the support role.") 190 | 191 | await context.channel.set_permissions(support_role, read_messages=True, send_messages=True) 192 | await context.send("Support role access has been restored to this ticket.") 193 | 194 | await ServerLogger.send_log( 195 | title="Ticket Downgraded", 196 | description=f"{context.author.mention} downgraded ticket {context.channel.name}", 197 | color=discord.Color.green(), 198 | guild=context.guild, 199 | channel=context.channel 200 | ) 201 | 202 | @ticket.command( 203 | name="add", 204 | description="Add a user to the ticket", 205 | usage="ticket add " 206 | ) 207 | @commands.check(Checks.is_not_blacklisted) 208 | @commands.check(Checks.command_not_disabled) 209 | async def add(self, context: Context, user: discord.Member): 210 | try: 211 | int(context.channel.topic.split()[0]) 212 | except: 213 | return await context.send("This is not a ticket channel.") 214 | 215 | member = context.guild.get_member(int(context.channel.topic.split(" ")[0])) 216 | 217 | if not context.author == member and not context.author.guild_permissions.manage_channels: 218 | return await context.send("You don't have permission to add users to this ticket.") 219 | 220 | if user in context.channel.members: 221 | return await context.send(f"{user.mention} is already in this ticket.") 222 | 223 | await context.channel.set_permissions(user, read_messages=True, send_messages=True) 224 | await context.send(f"Added {user.mention} to the ticket.") 225 | 226 | await ServerLogger.send_log( 227 | title="User Added to Ticket", 228 | description=f"{context.author.mention} added {user.mention} to ticket {context.channel.name}", 229 | color=discord.Color.blue(), 230 | guild=context.guild, 231 | channel=context.channel 232 | ) 233 | 234 | @ticket.command( 235 | name="remove", 236 | description="Remove a user from the ticket", 237 | usage="ticket remove " 238 | ) 239 | @commands.check(Checks.is_not_blacklisted) 240 | @commands.check(Checks.command_not_disabled) 241 | async def remove(self, context: Context, user: discord.Member): 242 | try: 243 | int(context.channel.topic.split()[0]) 244 | except: 245 | return await context.send("This is not a ticket channel.") 246 | 247 | member = context.guild.get_member(int(context.channel.topic.split(" ")[0])) 248 | 249 | if not context.author == member and not context.author.guild_permissions.manage_channels: 250 | return await context.send("You don't have permission to remove users from this ticket.") 251 | 252 | if user not in context.channel.members: 253 | return await context.send(f"{user.mention} is not in this ticket.") 254 | 255 | if user == context.channel.guild.get_member(int(context.channel.topic.split(" ")[0])): 256 | return await context.send("You can't remove the ticket creator.") 257 | 258 | await context.channel.set_permissions(user, overwrite=None) 259 | await context.send(f"Removed {user.mention} from the ticket.") 260 | 261 | await ServerLogger.send_log( 262 | title="User Removed from Ticket", 263 | description=f"{context.author.mention} removed {user.mention} from ticket {context.channel.name}", 264 | color=discord.Color.orange(), 265 | guild=context.guild, 266 | channel=context.channel 267 | ) 268 | 269 | @ticket.command( 270 | name="claim", 271 | description="Claim the ticket", 272 | usage="ticket claim" 273 | ) 274 | @commands.check(Checks.is_not_blacklisted) 275 | @commands.check(Checks.command_not_disabled) 276 | async def claim(self, context: Context): 277 | try: 278 | int(context.channel.topic.split()[0]) 279 | except: 280 | return await context.send("This is not a ticket channel.") 281 | 282 | member = context.guild.get_member(int(context.channel.topic.split(" ")[0])) 283 | 284 | if context.author == member: 285 | return await context.send("This is your ticket.") 286 | 287 | guilds = db["guilds"] 288 | guild = guilds.find_one({"id": context.guild.id}) 289 | 290 | if guild and guild.get("tickets_support_role"): 291 | support_role = context.guild.get_role(guild["tickets_support_role"]) 292 | 293 | await context.channel.set_permissions(member, overwrite=None) 294 | await context.channel.set_permissions(support_role, read_messages=True, send_messages=False) 295 | await context.channel.set_permissions(context.author, read_messages=True, send_messages=True) 296 | 297 | embed = discord.Embed( 298 | title="Ticket Claimed", 299 | description=f"{context.author.mention} will now handle this ticket.", 300 | ) 301 | 302 | await context.send(embed=embed) 303 | 304 | await ServerLogger.send_log( 305 | title="Ticket Claimed", 306 | description=f"{context.author.mention} claimed ticket {context.channel.name}", 307 | color=discord.Color.green(), 308 | guild=context.guild, 309 | channel=context.channel 310 | ) 311 | 312 | @ticket.command( 313 | name="unclaim", 314 | description="Unclaim the ticket", 315 | usage="ticket unclaim" 316 | ) 317 | @commands.check(Checks.is_not_blacklisted) 318 | @commands.check(Checks.command_not_disabled) 319 | async def unclaim(self, context: Context): 320 | try: 321 | int(context.channel.topic.split()[0]) 322 | except: 323 | return await context.send("This is not a ticket channel.") 324 | 325 | member = context.guild.get_member(int(context.channel.topic.split(" ")[0])) 326 | 327 | if context.author == member: 328 | return await context.send("This is your ticket.") 329 | 330 | guilds = db["guilds"] 331 | guild = guilds.find_one({"id": context.guild.id}) 332 | 333 | if guild and guild.get("tickets_support_role"): 334 | support_role = context.guild.get_role(guild["tickets_support_role"]) 335 | 336 | await context.channel.set_permissions(member, read_messages=True, send_messages=True) 337 | await context.channel.set_permissions(support_role, read_messages=True, send_messages=True) 338 | await context.channel.set_permissions(context.author, overwrite=None) 339 | 340 | embed = discord.Embed( 341 | title="Ticket Unclaimed", 342 | description=f"{context.author.mention} has unclaimed this ticket.", 343 | ) 344 | 345 | await context.send(embed=embed) 346 | 347 | 348 | @ticket.command( 349 | name="close", 350 | description="Close the ticket", 351 | usage="ticket close" 352 | ) 353 | @commands.check(Checks.is_not_blacklisted) 354 | @commands.check(Checks.command_not_disabled) 355 | async def close(self, context: Context): 356 | try: 357 | int(context.channel.topic.split()[0]) 358 | except: 359 | return await context.send("This is not a ticket channel.") 360 | 361 | member = context.guild.get_member(int(context.channel.topic.split(" ")[0])) 362 | 363 | if not context.author == member and not context.author.guild_permissions.manage_channels: 364 | return await context.send("You don't have permission to close this ticket.") 365 | 366 | await context.send("Starting ticket closing, dont run command again") 367 | 368 | os.makedirs("logs", exist_ok=True) 369 | log_file = f"logs/{context.channel.id}.log" 370 | with open(log_file, "w", encoding="UTF-8") as f: 371 | f.write( 372 | f'Ticket log 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' 373 | ) 374 | async for message in context.channel.history( 375 | limit=None, oldest_first=True 376 | ): 377 | attachments = [] 378 | for attachment in message.attachments: 379 | attachments.append(attachment.url) 380 | attachments_text = ( 381 | f"[Attached File{'s' if len(attachments) >= 2 else ''}: {', '.join(attachments)}]" 382 | if len(attachments) >= 1 383 | else "" 384 | ) 385 | f.write( 386 | f"{message.created_at.strftime('%d.%m.%Y %H:%M:%S')} {message.author} {message.id}: {message.clean_content} {attachments_text}\n" 387 | ) 388 | 389 | guilds = DBClient.client.potatobot["guilds"] 390 | data = guilds.find_one({"id": context.guild.id}) 391 | 392 | if data["log_channel"]: 393 | log_channel = context.guild.get_channel(data["log_channel"]) 394 | 395 | if log_channel: 396 | try: 397 | await log_channel.send(file=discord.File(log_file)) 398 | 399 | embed = discord.Embed( 400 | title="Ticket Closed", 401 | description=f"Ticket {context.channel.name} closed by {context.author.mention}", 402 | color=discord.Color.orange() 403 | ) 404 | 405 | await log_channel.send(embed=embed) 406 | except Exception as e: 407 | return await context.send("An error occurred, " + str(e)) 408 | 409 | try: 410 | with open (log_file, "rb") as f: 411 | await member.send(f"Your ticket in {context.guild} has been closed. Transcript: ", file=discord.File(f)) 412 | except Exception as e: 413 | await context.channel.send( 414 | f"Couldn't send the log file to {member.mention}, " + str(e) 415 | ) 416 | 417 | await context.channel.set_permissions(member, overwrite=None) 418 | await context.channel.edit(name=f"closed-{context.channel.name}") 419 | 420 | os.remove(log_file) 421 | 422 | await context.channel.send( 423 | embed= discord.Embed( 424 | description="Ticket Closed!", 425 | color = discord.Color.red() 426 | ), 427 | view = TrashButton() 428 | ) 429 | 430 | async def setup(bot) -> None: 431 | await bot.add_cog(Ticket(bot)) 432 | bot.add_view(CreateButton()) 433 | bot.add_view(CloseButton()) 434 | bot.add_view(TrashButton()) 435 | -------------------------------------------------------------------------------- /ui/setup.py: -------------------------------------------------------------------------------- 1 | import discord 2 | import asyncio 3 | 4 | from utils import DBClient 5 | 6 | db = DBClient.db 7 | 8 | class StartSetupView(discord.ui.View): 9 | def __init__(self, server_id) -> None: 10 | super().__init__() 11 | self.server_id = server_id 12 | 13 | @discord.ui.button(label="Start Setup", style=discord.ButtonStyle.primary) 14 | async def start_setup(self, interaction: discord.Interaction, button: discord.ui.Button) -> None: 15 | if interaction.user != interaction.guild.owner: 16 | await interaction.response.send_message("You can't interact with this :D", ephemeral=True) 17 | return 18 | 19 | embed = discord.Embed( 20 | title="Would you like to set up the ticket system?", 21 | description="This will allow people to create tickets for support.", 22 | color=0x2F3136 23 | ) 24 | 25 | await interaction.response.edit_message(embed=embed, view=TicketSetupView(self.server_id)) 26 | 27 | class TicketSetupView(discord.ui.View): 28 | def __init__(self, server_id) -> None: 29 | super().__init__() 30 | self.server_id = server_id 31 | 32 | @discord.ui.button(label="Yes", style=discord.ButtonStyle.primary) 33 | async def yes(self, interaction: discord.Interaction, button: discord.ui.Button) -> None: 34 | if interaction.user != interaction.guild.owner: 35 | await interaction.response.send_message("You can't interact with this :D", ephemeral=True) 36 | return 37 | 38 | embed = discord.Embed( 39 | title="What category should the tickets be created in?", 40 | description="Select the category where the tickets should be created.", 41 | color=0x2F3136 42 | ) 43 | 44 | categories = [discord.SelectOption(label=category.name, value=category.id) for category in interaction.guild.categories] 45 | 46 | await interaction.response.edit_message(embed=embed, view=TicketCategoryView(self.server_id, categories)) 47 | 48 | @discord.ui.button(label="Skip", style=discord.ButtonStyle.secondary) 49 | async def skip(self, interaction: discord.Interaction, button: discord.ui.Button) -> None: 50 | embed = discord.Embed( 51 | title="Change leveling system settings", 52 | description="Would you like to change the leveling system settings for your server?", 53 | color=0x2F3136 54 | ) 55 | 56 | await interaction.response.edit_message(embed=embed, view=LevelingSetupView(self.server_id)) 57 | 58 | class TicketCategorySelect(discord.ui.Select): 59 | def __init__(self, server_id, categories) -> None: 60 | super().__init__(placeholder="Choose a category...", options=categories) 61 | self.server_id = server_id 62 | 63 | async def callback(self, interaction: discord.Interaction) -> None: 64 | if interaction.user != interaction.guild.owner: 65 | await interaction.response.send_message("You can't interact with this :D", ephemeral=True) 66 | return 67 | 68 | category_id = self.values[0] 69 | db.guilds.update_one({"id": self.server_id}, {"$set": {"tickets_category": int(category_id)}}) 70 | 71 | embed = discord.Embed( 72 | title="What role should be given access to the tickets and pinged?", 73 | description="This can be a role like `Support` or `Staff`. You can mention the role to select it.", 74 | color=0x2F3136 75 | ) 76 | 77 | await interaction.response.edit_message(embed=embed, view=None) 78 | 79 | while True: 80 | try: 81 | message = await interaction.client.wait_for("message", check=lambda m: m.author == interaction.user, timeout=30) 82 | except asyncio.TimeoutError: 83 | await interaction.followup.send("You took too long to respond.", ephemeral=True) 84 | return 85 | 86 | try: 87 | role_id = int(message.content.replace("<@&", "").replace(">", "")) 88 | role = interaction.guild.get_role(role_id) 89 | 90 | if role is None: 91 | await interaction.followup.send("You must mention a role.", ephemeral=True) 92 | continue 93 | break 94 | except: 95 | await interaction.followup.send("You must mention a role.", ephemeral=True) 96 | 97 | await message.delete() 98 | db.guilds.update_one({"id": self.server_id}, {"$set": {"tickets_support_role": role_id}}) 99 | 100 | embed = discord.Embed( 101 | title="Change leveling system settings", 102 | description="Would you like to change the leveling system settings for your server?", 103 | color=0x2F3136 104 | ) 105 | 106 | await interaction.message.edit(embed=embed, view=LevelingSetupView(self.server_id)) 107 | 108 | class TicketCategoryView(discord.ui.View): 109 | def __init__(self, server_id, categories) -> None: 110 | super().__init__() 111 | self.server_id = server_id 112 | self.categories = categories 113 | 114 | self.add_item(TicketCategorySelect(self.server_id, self.categories)) 115 | 116 | class TicketSupportRoleSelect(discord.ui.Select): 117 | def __init__(self, server_id, roles) -> None: 118 | super().__init__(placeholder="Choose a role...", options=roles) 119 | self.server_id = server_id 120 | 121 | async def callback(self, interaction: discord.Interaction) -> None: 122 | if interaction.user != interaction.guild.owner: 123 | await interaction.response.send_message("You can't interact with this :D", ephemeral=True) 124 | return 125 | 126 | role_id = self.values[0] 127 | db.guilds.update_one({"id": self.server_id}, {"$set": {"tickets_support_role": role_id}}) 128 | 129 | embed = discord.Embed( 130 | title="Change leveling system settings", 131 | description="Would you like to change the leveling system settings for your server?", 132 | color=0x2F3136 133 | ) 134 | 135 | await interaction.response.edit_message(embed=embed, view=LevelingSetupView(self.server_id)) 136 | 137 | class TicketSupportRoleView(discord.ui.View): 138 | def __init__(self, server_id, roles) -> None: 139 | super().__init__() 140 | self.server_id = server_id 141 | 142 | self.add_item(TicketSupportRoleSelect(self.server_id, roles)) 143 | 144 | class LevelingSetupView(discord.ui.View): 145 | def __init__(self, server_id) -> None: 146 | super().__init__() 147 | self.server_id = server_id 148 | 149 | @discord.ui.button(label="Yes", style=discord.ButtonStyle.primary) 150 | async def yes(self, interaction: discord.Interaction, button: discord.ui.Button) -> None: 151 | if interaction.user != interaction.guild.owner: 152 | await interaction.response.send_message("You can't interact with this :D", ephemeral=True) 153 | return 154 | 155 | embed = discord.Embed( 156 | title="Should levelups be announced?", 157 | description="Tell when someone levels up", 158 | color=0x2F3136 159 | ) 160 | 161 | await interaction.response.edit_message(embed=embed, view=LevelingShouldAnnounceLevelUp(self.server_id)) 162 | 163 | @discord.ui.button(label="No", style=discord.ButtonStyle.secondary) 164 | async def no(self, interaction: discord.Interaction, button: discord.ui.Button) -> None: 165 | db.guilds.update_one({"id": self.server_id}, {"$set": {"should_announce_levelup": False}}) 166 | 167 | embed = discord.Embed( 168 | title="Setup starboard?", 169 | description="Would you like to setup the starboard?", 170 | color=0x2F3136 171 | ) 172 | 173 | await interaction.response.edit_message(embed=embed, view=StarboardSetupView(self.server_id)) 174 | 175 | class LevelingShouldAnnounceLevelUp(discord.ui.View): 176 | def __init__(self, server_id) -> None: 177 | super().__init__() 178 | self.server_id = server_id 179 | 180 | @discord.ui.button(label="Yes", style=discord.ButtonStyle.primary) 181 | async def yes(self, interaction: discord.Interaction, button: discord.ui.Button) -> None: 182 | if interaction.user != interaction.guild.owner: 183 | await interaction.response.send_message("You can't interact with this :D", ephemeral=True) 184 | return 185 | 186 | db.guilds.update_one({"id": self.server_id}, {"$set": {"should_announce_levelup": True}}) 187 | 188 | embed = discord.Embed( 189 | title="Would you like to set a channel for levelups?", 190 | description="Which channel to send levelup messages, will be sent in the channel where the user leveled up if not set. Mention the channel to select it.", 191 | color=0x2F3136 192 | ) 193 | 194 | channels = [discord.SelectOption(label=channel.name, value=channel.id) for channel in interaction.guild.text_channels] 195 | 196 | await interaction.response.edit_message(embed=embed, view=LevelingChannelSelectView(self.server_id, channels)) 197 | 198 | @discord.ui.button(label="No", style=discord.ButtonStyle.secondary) 199 | async def no(self, interaction: discord.Interaction, button: discord.ui.Button) -> None: 200 | db.guilds.update_one({"id": self.server_id}, {"$set": {"should_announce_levelup": False}}) 201 | 202 | embed = discord.Embed( 203 | title="Setup starboard?", 204 | description="Would you like to setup the starboard?", 205 | color=0x2F3136 206 | ) 207 | 208 | await interaction.response.edit_message(embed=embed, view=StarboardSetupView(self.server_id)) 209 | 210 | class LevelingChannelSelectView(discord.ui.View): 211 | def __init__(self, server_id, channels) -> None: 212 | super().__init__() 213 | self.server_id = server_id 214 | 215 | @discord.ui.button(label="Yes", style=discord.ButtonStyle.primary) 216 | async def yes(self, interaction: discord.Interaction, button: discord.ui.Button) -> None: 217 | if interaction.user != interaction.guild.owner: 218 | await interaction.response.send_message("You can't interact with this :D", ephemeral=True) 219 | return 220 | 221 | embed = discord.Embed( 222 | title="Mention the channel for levelups", 223 | description="Which channel to send levelup messages, will be sent in the channel where the user leveled up if not set. Mention the channel.", 224 | color=0x2F3136 225 | ) 226 | 227 | await interaction.response.edit_message(embed=embed, view=None) 228 | 229 | while True: 230 | try: 231 | message = await interaction.client.wait_for("message", check=lambda m: m.author == interaction.user, timeout=30) 232 | except asyncio.TimeoutError: 233 | await interaction.followup.send("You took too long to respond.", ephemeral=True) 234 | return 235 | 236 | try: 237 | channel_id = int(message.content.replace("<#", "").replace(">", "")) 238 | channel = interaction.guild.get_channel(channel_id) 239 | 240 | if channel is None: 241 | await interaction.followup.send("You must mention a channel.", ephemeral=True) 242 | continue 243 | break 244 | except: 245 | await interaction.followup.send("You must mention a channel.", ephemeral=True) 246 | 247 | await message.delete() 248 | db.guilds.update_one({"id": self.server_id}, {"$set": {"level_announce_channel": channel_id}}) 249 | 250 | embed = discord.Embed( 251 | title="Setup starboard?", 252 | description="Would you like to setup the starboard?", 253 | color=0x2F3136 254 | ) 255 | 256 | await interaction.message.edit(embed=embed, view=StarboardSetupView(self.server_id)) 257 | 258 | @discord.ui.button(label="No", style=discord.ButtonStyle.secondary) 259 | async def no(self, interaction: discord.Interaction, button: discord.ui.Button) -> None: 260 | embed = discord.Embed( 261 | title="Setup starboard?", 262 | description="Would you like to setup the starboard?", 263 | color=0x2F3136 264 | ) 265 | 266 | await interaction.response.edit_message(embed=embed, view=StarboardSetupView(self.server_id)) 267 | 268 | class StarboardSetupView(discord.ui.View): 269 | def __init__(self, server_id) -> None: 270 | super().__init__() 271 | self.server_id = server_id 272 | 273 | @discord.ui.button(label="Yes", style=discord.ButtonStyle.primary) 274 | async def yes(self, interaction: discord.Interaction, button: discord.ui.Button) -> None: 275 | if interaction.user != interaction.guild.owner: 276 | await interaction.response.send_message("You can't interact with this :D", ephemeral=True) 277 | return 278 | 279 | db.guilds.update_one({"id": self.server_id}, {"$set": {"starboard.enabled": True}}) 280 | 281 | embed = discord.Embed( 282 | title="Mention the channel for the starboard", 283 | description="Send a message containing the channel where the starboard should be created", 284 | color=0x2F3136 285 | ) 286 | 287 | await interaction.response.edit_message(embed=embed, view=None) 288 | 289 | while True: 290 | try: 291 | message = await interaction.client.wait_for("message", check=lambda m: m.author == interaction.user, timeout=30) 292 | except asyncio.TimeoutError: 293 | await interaction.followup.send("You took too long to respond.", ephemeral=True) 294 | return 295 | 296 | try: 297 | channel_id = int(message.content.replace("<#", "").replace(">", "")) 298 | channel = interaction.guild.get_channel(channel_id) 299 | 300 | if channel is None: 301 | await interaction.followup.send("You must mention a channel.", ephemeral=True) 302 | continue 303 | break 304 | except: 305 | await interaction.followup.send("You must mention a channel.", ephemeral=True) 306 | 307 | await message.delete() 308 | db.guilds.update_one({"id": self.server_id}, {"$set": {"starboard.channel": channel_id}}) 309 | 310 | embed = discord.Embed( 311 | title="Select the starboard threshold", 312 | description="Send a message containing the threshold for the starboard.", 313 | color=0x2F3136 314 | ) 315 | 316 | await interaction.message.edit(embed=embed, view=None) 317 | 318 | while True: 319 | try: 320 | message = await interaction.client.wait_for("message", check=lambda m: m.author == interaction.user, timeout=30) 321 | except asyncio.TimeoutError: 322 | await interaction.followup.send("You took too long to respond.", ephemeral=True) 323 | return 324 | 325 | if not message.content.isdigit(): 326 | await interaction.followup.send("You must send a number.", ephemeral=True) 327 | continue 328 | 329 | break 330 | 331 | threshold = int(message.content) 332 | 333 | await message.delete() 334 | db.guilds.update_one({"id": self.server_id}, {"$set": {"starboard.threshold": threshold}}) 335 | 336 | embed = discord.Embed( 337 | title = "Do you want to set a logging channel?", 338 | description = "Would you like to set a logging channel for mod/admin actions?", 339 | color = 0x2F3136 340 | ) 341 | 342 | await interaction.message.edit(embed=embed, view=LoggingSetupView(self.server_id)) 343 | 344 | @discord.ui.button(label="No", style=discord.ButtonStyle.secondary) 345 | async def no(self, interaction: discord.Interaction, button: discord.ui.Button) -> None: 346 | db.guilds.update_one({"id": self.server_id}, {"$set": {"starboard.enabled": False}}) 347 | 348 | embed = discord.Embed( 349 | title = "Do you want to set a logging channel?", 350 | description = "Would you like to set a logging channel for mod/admin actions?", 351 | color = 0x2F3136 352 | ) 353 | 354 | await interaction.response.edit_message(embed=embed, view=LoggingSetupView(self.server_id)) 355 | 356 | class LoggingSetupView(discord.ui.View): 357 | def __init__(self, server_id) -> None: 358 | super().__init__() 359 | self.server_id = server_id 360 | 361 | @discord.ui.button(label="Yes", style=discord.ButtonStyle.primary) 362 | async def yes(self, interaction: discord.Interaction, button: discord.ui.Button) -> None: 363 | if interaction.user != interaction.guild.owner: 364 | await interaction.response.send_message("You can't interact with this :D", ephemeral=True) 365 | return 366 | 367 | embed = discord.Embed( 368 | title="Mention the logging channel", 369 | description="Send a message containing the channel where logs should be sent", 370 | color=0x2F3136 371 | ) 372 | 373 | await interaction.response.edit_message(embed=embed, view=None) 374 | 375 | while True: 376 | try: 377 | message = await interaction.client.wait_for("message", check=lambda m: m.author == interaction.user, timeout=30) 378 | except asyncio.TimeoutError: 379 | await interaction.followup.send("You took too long to respond.", ephemeral=True) 380 | return 381 | 382 | try: 383 | channel_id = int(message.content.replace("<#", "").replace(">", "")) 384 | channel = interaction.guild.get_channel(channel_id) 385 | 386 | if channel is None: 387 | await interaction.followup.send("You must mention a channel.", ephemeral=True) 388 | continue 389 | break 390 | except: 391 | await interaction.followup.send("You must mention a channel.", ephemeral=True) 392 | 393 | await message.delete() 394 | db.guilds.update_one({"id": self.server_id}, {"$set": {"log_channel": channel_id}}) 395 | 396 | embed = discord.Embed( 397 | title="Setup complete!", 398 | description="We recommend you move the role 'Potato Bot' high up on the role list to make sure all features works properly", 399 | color=0x2F3136 400 | ) 401 | 402 | await interaction.followup.send(embed=embed) 403 | 404 | @discord.ui.button(label="No", style=discord.ButtonStyle.secondary) 405 | async def no(self, interaction: discord.Interaction, button: discord.ui.Button) -> None: 406 | embed = discord.Embed( 407 | title="Setup complete!", 408 | description="We recommend you move the role 'Potato Bot' high up on the role list to make sure all features works properly", 409 | color=0x2F3136 410 | ) 411 | 412 | await interaction.response.edit_message(embed=embed) 413 | -------------------------------------------------------------------------------- /cogs/server.py: -------------------------------------------------------------------------------- 1 | # This project is licensed under the terms of the GPL v3.0 license. Copyright 2024 Cyteon 2 | 3 | import discord 4 | import os 5 | import aiohttp 6 | 7 | from cryptography.fernet import Fernet 8 | 9 | from discord.ext import commands 10 | from discord.ext.commands import Context 11 | 12 | from utils import CachedDB, Checks, DBClient, CONSTANTS 13 | from ui.setup import StartSetupView 14 | 15 | 16 | db = DBClient.db 17 | 18 | class Server(commands.Cog, name="⚙️ Server"): 19 | def __init__(self, bot) -> None: 20 | self.bot = bot 21 | self.prefixDB = bot.prefixDB 22 | 23 | @commands.hybrid_command( 24 | name="setup", 25 | description="It's setup time!!!!!!", 26 | usage="testcommand" 27 | ) 28 | @commands.check(Checks.is_not_blacklisted) 29 | async def setup(self, context: Context) -> None: 30 | if context.author.id != context.guild.owner.id: 31 | await context.send("You must be the owner of the server to run this command.") 32 | return 33 | 34 | embed = discord.Embed( 35 | title="Setup", 36 | description="Let's set up your server!", 37 | color=0x2F3136 38 | ) 39 | 40 | await context.send(embed=embed, view=StartSetupView(context.guild.id)) 41 | 42 | @commands.command( 43 | name="prefix", 44 | description="Change the bot prefix", 45 | usage="prefix " 46 | ) 47 | @commands.check(Checks.is_not_blacklisted) 48 | @commands.has_permissions(manage_channels=True) 49 | async def prefix(self, context: commands.Context, prefix: str = "none"): 50 | if prefix == "none": 51 | return await context.send("Current prefix is: `" + self.prefixDB.get(str(context.guild.id)) + "`") 52 | 53 | if prefix == "/": 54 | return await context.send("Prefix cannot be `/`") 55 | 56 | guild_id = str(context.guild.id) 57 | self.prefixDB.set(guild_id, prefix) 58 | self.prefixDB.save() 59 | await context.send(f"Prefix set to {prefix}") 60 | 61 | @commands.hybrid_command( 62 | name="groq-api-key", 63 | description="Set API key for AI (run in private channel please)", 64 | usage="groq-api-key " 65 | ) 66 | @commands.check(Checks.is_not_blacklisted) 67 | @commands.has_permissions(manage_channels=True) 68 | async def groq_api_key(self, context: commands.Context, key: str): 69 | c = db["guilds"] 70 | data = c.find_one({"id": context.guild.id}) 71 | 72 | if not data: 73 | data = CONSTANTS.guild_data_template(context.guild.id) 74 | c.insert_one(data) 75 | 76 | cipher_suite = Fernet(os.getenv("HASHING_SECRET")) 77 | cipher_text = cipher_suite.encrypt(key.encode()) 78 | 79 | try: 80 | await context.message.delete() 81 | except: 82 | pass 83 | 84 | if key == "NONE": 85 | cipher_text = "NONE" 86 | 87 | newdata = { "$set": { "groq_api_key": cipher_text } } 88 | 89 | c.update_one({"id": context.guild.id}, newdata) 90 | 91 | await context.send(f"Set groq api key") 92 | 93 | @commands.hybrid_command( 94 | name="stealemoji", 95 | description="Steal an emoji from another server.", 96 | usage="stealemoji " 97 | ) 98 | @commands.check(Checks.is_not_blacklisted) 99 | @commands.has_permissions(manage_emojis=True) 100 | @commands.bot_has_permissions(manage_emojis=True) 101 | async def stealemoji(self, context: Context, emoji: discord.PartialEmoji, name: str) -> None: 102 | try: 103 | emoji_bytes = await emoji.read() 104 | await context.guild.create_custom_emoji( 105 | name=name if name else emoji.name, 106 | image=emoji_bytes, 107 | reason=f"Emoji yoinked by {context.author} VIA {context.guild.me.name}", 108 | ) 109 | await context.send( 110 | embed=discord.Embed( 111 | description=f"Emoji Stolen", 112 | color=discord.Color.random(), 113 | ).set_image(url=emoji.url) 114 | ) 115 | except Exception as e: 116 | await context.send(str(e)) 117 | 118 | @commands.hybrid_command( 119 | name="emojifromurl", 120 | description="Add an emoji from a URL.", 121 | usage="emojifromurl " 122 | ) 123 | @commands.check(Checks.is_not_blacklisted) 124 | @commands.has_permissions(manage_emojis=True) 125 | @commands.bot_has_permissions(manage_emojis=True) 126 | async def emojifromurl(self, context: Context, url: str, name: str) -> None: 127 | async with aiohttp.ClientSession() as session: 128 | async with session.get(url) as response: 129 | if response.status == 200: 130 | emoji_bytes = await response.read() 131 | await context.guild.create_custom_emoji( 132 | name=name, 133 | image=emoji_bytes, 134 | reason=f"Emoji added by {context.author} VIA {context.guild.me.name}", 135 | ) 136 | await context.send( 137 | embed=discord.Embed( 138 | description=f"Emoji added", 139 | color=discord.Color.random(), 140 | ).set_image(url=url) 141 | ) 142 | else: 143 | await context.send("Failed to download the emoji") 144 | 145 | @commands.hybrid_group( 146 | name="settings", 147 | description="Command to change server settings", 148 | aliases=["setting"], 149 | usage="settings [args]" 150 | ) 151 | @commands.check(Checks.is_not_blacklisted) 152 | @commands.has_permissions(manage_channels=True) 153 | async def settings(self, context: Context) -> None: 154 | subcommands = [cmd for cmd in self.settings.walk_commands()] 155 | 156 | data = [] 157 | 158 | for subcommand in subcommands: 159 | description = subcommand.description.partition("\n")[0] 160 | data.append(f"{await self.bot.get_prefix(context)}settings {subcommand.name} - {description}") 161 | 162 | help_text = "\n".join(data) 163 | embed = discord.Embed( 164 | title=f"Help: Settings", description="List of available commands:", color=0xBEBEFE 165 | ) 166 | embed.add_field( 167 | name="Commands", value=f"```{help_text}```", inline=False 168 | ) 169 | 170 | await context.send(embed=embed) 171 | 172 | @settings.command( 173 | name="show", 174 | description="Show server settings", 175 | usage="settings show" 176 | ) 177 | @commands.check(Checks.is_not_blacklisted) 178 | @commands.has_permissions(manage_channels=True) 179 | async def show(self, context: Context) -> None: 180 | c = db["guilds"] 181 | data = c.find_one({"id": context.guild.id}) 182 | 183 | if not data: 184 | data = CONSTANTS.guild_data_template(context.guild.id) 185 | c.insert_one(data) 186 | 187 | embed = discord.Embed( 188 | title="Server Settings", 189 | color=discord.Color.blue() 190 | ) 191 | 192 | embed.add_field( name="Daily Cash", value=data["daily_cash"] ) 193 | embed.add_field( name="Tickets Category", 194 | value=context.guild.get_channel(data["tickets_category"]).name.capitalize() if data["tickets_category"] else "None" ) 195 | embed.add_field( name="Tickets Support Role", 196 | value=context.guild.get_role(data["tickets_support_role"]).mention if data["tickets_support_role"] else "None" ) 197 | embed.add_field( name="Log Channel", value=context.guild.get_channel(data["log_channel"]).mention if data["log_channel"] else "None" ) 198 | embed.add_field( name="Level Roles", value="`/setting level_roles show`") 199 | embed.add_field( name="Level Announce Channel", 200 | value=context.guild.get_channel( data["level_announce_channel"]).mention if ( 201 | "level_announce_channel" in data and context.guild.get_channel(data["level_announce_channel"]) != None 202 | ) else "None" 203 | ) 204 | embed.add_field( name="Should announce levelup", value=data["should_announce_levelup"] if "should_announce_levelup" in data else "idk") 205 | 206 | await context.send(embed=embed) 207 | 208 | @settings.command( 209 | name="announce-levelup", 210 | description="Should levelups be announced?", 211 | usage="settings announce-levelup " 212 | ) 213 | @commands.check(Checks.is_not_blacklisted) 214 | @commands.has_permissions(manage_roles=True) 215 | async def should_announce_levelup(self, context: Context, enabled: bool) -> None: 216 | c = db["guilds"] 217 | data = c.find_one({"id": context.guild.id}) 218 | 219 | if not data: 220 | data = CONSTANTS.guild_data_template(context.guild.id) 221 | c.insert_one(data) 222 | 223 | newdata = { "$set": { "should_announce_levelup": enabled } } 224 | 225 | c.update_one({"id": context.guild.id}, newdata) 226 | 227 | await context.send(f"Set should announce levelup to {enabled}") 228 | 229 | @settings.command( 230 | name="daily-cash", 231 | description="Set daily cash amount", 232 | usage="settings daily-cash " 233 | ) 234 | @commands.check(Checks.is_not_blacklisted) 235 | @commands.has_permissions(administrator=True) 236 | async def daily_cash(self, context: Context, amount: int) -> None: 237 | c = db["guilds"] 238 | 239 | data = c.find_one({"id": context.guild.id}) 240 | 241 | if not data: 242 | data = CONSTANTS.guild_data_template(context.guild.id) 243 | c.insert_one(data) 244 | 245 | newdata = { "$set": { "daily_cash": amount } } 246 | 247 | c.update_one({"id": context.guild.id}, newdata) 248 | 249 | await context.send(f"Set daily cash to {amount}") 250 | 251 | @settings.command( 252 | name="tickets-category", 253 | description="Set category where tickets are created", 254 | usage="settings tickets-category " 255 | ) 256 | @commands.check(Checks.is_not_blacklisted) 257 | @commands.has_permissions(administrator=True) 258 | async def tickets_category(self, context: Context, category: discord.CategoryChannel) -> None: 259 | c = db["guilds"] 260 | 261 | data = c.find_one({"id": context.guild.id}) 262 | 263 | if not data: 264 | data = CONSTANTS.guild_data_template(context.guild.id) 265 | c.insert_one(data) 266 | 267 | newdata = { "$set": { "tickets_category": category.id } } 268 | 269 | c.update_one({"id": context.guild.id}, newdata) 270 | 271 | await context.send(f"Set tickets category to {category.mention}") 272 | 273 | @settings.command( 274 | name="level-up-channel", 275 | description="Set level up announce channel", 276 | usage="settings level-up-channel " 277 | ) 278 | @commands.check(Checks.is_not_blacklisted) 279 | @commands.has_permissions(manage_channels=True) 280 | async def level_up_channel(self, context: Context, channel: discord.TextChannel) -> None: 281 | c = db["guilds"] 282 | 283 | data = c.find_one({"id": context.guild.id}) 284 | 285 | if not data: 286 | data = CONSTANTS.guild_data_template(context.guild.id) 287 | c.insert_one(data) 288 | 289 | newdata = { "$set": { "level_announce_channel": channel.id } } 290 | 291 | c.update_one({"id": context.guild.id}, newdata) 292 | 293 | await context.send(f"Set level announce channel to {channel.mention}") 294 | 295 | @settings.command( 296 | name="tickets-support-role", 297 | description="Set ticket support role", 298 | usage="settings tickets-support-role " 299 | ) 300 | @commands.check(Checks.is_not_blacklisted) 301 | @commands.has_permissions(manage_roles=True) 302 | async def tickets_support_role(self, context: Context, role: discord.Role) -> None: 303 | c = db["guilds"] 304 | 305 | data = c.find_one({"id": context.guild.id}) 306 | 307 | if not data: 308 | data = CONSTANTS.guild_data_template(context.guild.id) 309 | c.insert_one(data) 310 | 311 | newdata = { "$set": { "tickets_support_role": role.id } } 312 | 313 | c.update_one({"id": context.guild.id}, newdata) 314 | 315 | await context.send(f"Set tickets support role to {role.mention}") 316 | 317 | @settings.command( 318 | name="log-channel", 319 | description="Set log channel", 320 | usage="settings log-channel " 321 | ) 322 | @commands.check(Checks.is_not_blacklisted) 323 | @commands.has_permissions(manage_channels=True) 324 | async def log_channel(self, context: Context, channel: discord.TextChannel) -> None: 325 | c = db["guilds"] 326 | 327 | data = c.find_one({"id": context.guild.id}) 328 | 329 | if not data: 330 | data = CONSTANTS.guild_data_template(context.guild.id) 331 | c.insert_one(data) 332 | 333 | newdata = { "$set": { "log_channel": channel.id } } 334 | 335 | c.update_one({"id": context.guild.id}, newdata) 336 | 337 | await context.send(f"Set log channel to {channel.mention}") 338 | 339 | @settings.command( 340 | name="default-role", 341 | description="Set default role to be given to new members", 342 | usage="settings default-role " 343 | ) 344 | @commands.check(Checks.is_not_blacklisted) 345 | @commands.has_permissions(manage_roles=True) 346 | async def default_role(self, context: Context, role: discord.Role) -> None: 347 | c = db["guilds"] 348 | 349 | data = c.find_one({"id": context.guild.id}) 350 | 351 | if not data: 352 | data = CONSTANTS.guild_data_template(context.guild.id) 353 | c.insert_one(data) 354 | 355 | dangerous_permissions = [ 356 | "administrator", 357 | "manage_guild", 358 | "manage_roles", 359 | "manage_channels", 360 | "manage_messages", 361 | "kick_members", 362 | "ban_members", 363 | "manage_webhooks", 364 | "manage_emojis", 365 | "manage_nicknames", 366 | ] 367 | 368 | for permission in dangerous_permissions: 369 | if getattr(role.permissions, permission): 370 | return await context.send("The role has dangerous permissions. Please choose a role without dangerous permissions.") 371 | 372 | newdata = { "$set": { "default_role": role.id } } 373 | 374 | c.update_one({"id": context.guild.id}, newdata) 375 | 376 | await context.send(f"Set default role to {role.name}") 377 | 378 | @settings.group( 379 | name="level-roles", 380 | description="Commands to set up level roles", 381 | usage="settings level-roles" 382 | ) 383 | @commands.check(Checks.is_not_blacklisted) 384 | @commands.has_permissions(manage_roles=True) 385 | async def level_roles(self, context: Context) -> None: 386 | c = db["guilds"] 387 | data = c.find_one({"id": context.guild.id}) 388 | 389 | if not data: 390 | data = CONSTANTS.guild_data_template(context.guild.id) 391 | c.insert_one(data) 392 | 393 | embed = discord.Embed( 394 | title="Level Roles", 395 | color=discord.Color.blue() 396 | ) 397 | 398 | 399 | for r in data["level_roles"]: 400 | embed.add_field( 401 | name=r, 402 | value=context.guild.get_role(data["level_roles"][r]).mention 403 | ) 404 | 405 | await context.send(embed=embed) 406 | 407 | @level_roles.command( 408 | name="show", 409 | description="Show level roles", 410 | usage="settings level-roles show" 411 | ) 412 | @commands.check(Checks.is_not_blacklisted) 413 | @commands.has_permissions(manage_roles=True) 414 | async def show_level_roles(self, context: Context) -> None: 415 | c = db["guilds"] 416 | data = c.find_one({"id": context.guild.id}) 417 | 418 | if not data: 419 | data = CONSTANTS.guild_data_template(context.guild.id) 420 | c.insert_one(data) 421 | 422 | embed = discord.Embed( 423 | title="Level Roles", 424 | color=discord.Color.blue() 425 | ) 426 | 427 | for r in data["level_roles"]: 428 | embed.add_field( 429 | name=r, 430 | value=context.guild.get_role(data["level_roles"][r]).mention 431 | ) 432 | 433 | await context.send(embed=embed) 434 | 435 | @level_roles.command( 436 | name="set", 437 | description="Set level roles", 438 | usage="settings level-roles set " 439 | ) 440 | @commands.check(Checks.is_not_blacklisted) 441 | @commands.has_permissions(manage_roles=True) 442 | async def set(self, context: Context, level: int, role: discord.Role) -> None: 443 | c = db["guilds"] 444 | data = c.find_one({"id": context.guild.id}) 445 | 446 | if not data: 447 | data = CONSTANTS.guild_data_template(context.guild.id) 448 | c.insert_one(data) 449 | 450 | level_roles = data["level_roles"] 451 | level_roles[str(level)] = role.id 452 | 453 | newdata = { "$set": { "level_roles": level_roles } } 454 | c.update_one({"id": context.guild.id}, newdata) 455 | 456 | await context.send(f"Set level {level} role to {role.name}") 457 | 458 | @commands.hybrid_group( 459 | name="command", 460 | description="Commands to re-enable/disable commands", 461 | aliases=["cmd"], 462 | usage="Command [args]" 463 | ) 464 | @commands.check(Checks.is_not_blacklisted) 465 | @commands.has_permissions(manage_channels=True) 466 | async def cmd(self, context: Context) -> None: 467 | prefix = await self.bot.get_prefix(context) 468 | 469 | cmds = "\n".join([f"{prefix}cmd {cmd.name} - {cmd.description}" for cmd in self.cmd.walk_commands()]) 470 | 471 | embed = discord.Embed( 472 | title=f"Help: Command", description="List of available commands:", color=0xBEBEFE 473 | ) 474 | embed.add_field( 475 | name="Commands", value=f"```{cmds}```", inline=False 476 | ) 477 | 478 | await context.send(embed=embed) 479 | 480 | @cmd.command( 481 | name="disable", 482 | description="Disable a command", 483 | usage="cmd disable " 484 | ) 485 | @commands.check(Checks.is_not_blacklisted) 486 | @commands.has_permissions(administrator=True) 487 | async def disable(self, context: Context, *, command: str) -> None: 488 | cmd = self.bot.get_command(command) 489 | 490 | if not cmd: 491 | return await context.send("Command not found") 492 | 493 | if cmd.qualified_name.startswith("command") or cmd.qualified_name.startswith("cmd"): 494 | return await context.send("You cannot disable this command") 495 | 496 | guild = await CachedDB.find_one(db["guilds"], {"id": context.guild.id}) 497 | 498 | if not guild: 499 | guild = CONSTANTS.guild_data_template(context.guild.id) 500 | db["guilds"].insert_one(guild) 501 | 502 | if cmd.qualified_name in guild["disabled_commands"]: 503 | return await context.send(f"The command `{cmd.qualified_name}` is already disabled") 504 | 505 | guild["disabled_commands"].append(cmd.qualified_name) 506 | 507 | await CachedDB.update_one(db["guilds"], {"id": context.guild.id}, {"$set": {"disabled_commands": guild["disabled_commands"]}}) 508 | 509 | await context.send(f"Disabled the command `{cmd.qualified_name}`") 510 | 511 | @cmd.command( 512 | name="enable", 513 | description="Re-enable a command", 514 | usage="cmd enable " 515 | ) 516 | @commands.check(Checks.is_not_blacklisted) 517 | @commands.has_permissions(administrator=True) 518 | async def cmd_enable(self, context: Context, *, command: str) -> None: 519 | cmd = self.bot.get_command(command) 520 | 521 | if not cmd: 522 | return await context.send("Command not found") 523 | 524 | guild = await CachedDB.find_one(db["guilds"], {"id": context.guild.id}) 525 | 526 | if not guild: 527 | guild = CONSTANTS.guild_data_template(context.guild.id) 528 | db["guilds"].insert_one(guild) 529 | 530 | if command not in guild["disabled_commands"]: 531 | return await context.send(f"The command `{cmd.qualified_name}` is not disabled") 532 | 533 | guild["disabled_commands"].remove(cmd.qualified_name) 534 | 535 | await CachedDB.update_one(db["guilds"], {"id": context.guild.id}, {"$set": {"disabled_commands": guild["disabled_commands"]}}) 536 | 537 | await context.send(f"Re-enabled the command `{cmd.qualified_name}`") 538 | 539 | 540 | async def setup(bot) -> None: 541 | await bot.add_cog(Server(bot)) 542 | --------------------------------------------------------------------------------