diff --git a/.gitignore b/.gitignore index ca57c1a..8e50811 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ config.json .idea .vscode cogs/__pycache__ -__pycache__ \ No newline at end of file +__pycache__ +venv \ No newline at end of file diff --git a/CONFIGURATION.md b/CONFIGURATION.md index 499a1c2..caa6c0e 100644 --- a/CONFIGURATION.md +++ b/CONFIGURATION.md @@ -1,6 +1,6 @@ # Configuration Pingernos py offers three different ways to configure it, a config file, environment variables and a .env -If you wish to use the config file, you must set the `use_json` variable in [getdata](./utils.py) to `True` and copy the [example.config.json](./example.config.json) to a new file called config.json in the root folder and modify the options in it. +If you wish to use the config file, you must set the `use_json` variable in [getdata](./utilities/data.py) to `True` and copy the [example.config.json](./example.config.json) to a new file called config.json in the root folder and modify the options in it. If you wish to use a .env file, just copy the [example.env](./example.env) to a new file called .env in the root folder and modify the options in it, you can use environment variables and a .env file at the same time if you so desire. Environment variables use `SCREAMING_SNAKE_CASE` while the configuration file uses `PascalCase`. @@ -9,45 +9,45 @@ If a config file is missing, pingernos will try to load the environment variable ## Auth Token (required) Discord bot authentication token, can be generated in the [Developer Portal](https://discord.com/developers/applications/) -| type | config file | environment | -|--------|-------------|---------------------| -| string | `Token` | `TOKEN` | +| type | config file | environment | +|--------|-------------|-------------| +| string | `Token` | `TOKEN` | ## Prefix (required) Prefix to use for commands, can be a mention or a string -| type | config file | environment | -|--------|-------------|---------------------| -| string | `Prefix` | `PREFIX` | +| type | config file | environment | +|--------|-------------|-------------| +| string | `Prefix` | `PREFIX` | ## Owners (required) Array of user ids that are allowed to use owner only features -| type | config file | environment | -|----------|--------------------|----------------------------| -| string[] | `Owners` | `OWNERS` | +| type | config file | environment | +|----------|-------------|-------------| +| string[] | `Owners` | `OWNERS` | ## FeatureGuilds (required) Array of guild ids that are allowed to use owner only features -| type | config file | environment | -|----------|--------------------|----------------------------| -| string[] | `FeatureGuilds` | `FEATURE_GUILDS` | +| type | config file | environment | +|----------|------------------|------------------| +| string[] | `FeatureGuilds` | `FEATURE_GUILDS` | ## Logs (required) ### JoinWebhook (required) Webhook to send join logs to -| type | config file | environment | -|--------|-----------------|------------------------| +| type | config file | environment | +|--------|--------------------|--------------------| | string | `Logs.JoinWebhook` | `LOGS_JOINWEBHOOK` | ### LeaveWebhook (required) Webhook to send leave logs to -| type | config file | environment | -|--------|-----------------|------------------------| +| type | config file | environment | +|--------|---------------------|---------------------| | string | `Logs.LeaveWebhook` | `LOGS_LEAVEWEBHOOK` | ## Database (required) @@ -56,34 +56,34 @@ MySQL/MariaDB access credentials. Other SQL dialects or Databases are not suppor ### Host Database hostname or IP -| type | config file | environment | -|--------|-----------------|------------------------| -| string | `Database.Host` | `DB_HOST` | +| type | config file | environment | +|--------|-----------------|-------------| +| string | `Database.Host` | `DB_HOST` | ### User Database username -| type | config file | environment | -|--------|-----------------|------------------------| -| string | `Database.User` | `DB_USER` | +| type | config file | environment | +|--------|-----------------|-------------| +| string | `Database.User` | `DB_USER` | ### Password Database password -| type | config file | environment | -|--------|---------------------|----------------------------| +| type | config file | environment | +|--------|---------------------|---------------| | string | `Database.Password` | `DB_PASSWORD` | ### Database Database name -| type | config file | environment | -|--------|---------------------|----------------------------| +| type | config file | environment | +|--------|---------------------|---------------| | string | `Database.Database` | `DB_DATABASE` | ### Port Database port -| type | config file | environment | -|--------|-----------------|------------------------| -| int | `Database.Port` | `DB_PORT` | \ No newline at end of file +| type | config file | environment | +|--------|-----------------|-------------| +| int | `Database.Port` | `DB_PORT` | \ No newline at end of file diff --git a/SECURITY.md b/SECURITY.md index 5841a67..56c48fc 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,10 +4,10 @@ We only support the following versions with security updates: -*This'll usually be limited to the latest version.* +*This will usually be limited to the latest version.* | Version | Supported | -| ------- | ------------------ | +|---------|--------------------| | 2.3.x | :white_check_mark: | | < 2.0 | :x: | @@ -22,7 +22,7 @@ Amongst other things, things that counts as a security vulnerability include, bu ### How to report it We ask you to report any and all vulnerabilities to us in one of the following ways: -- Sending a mail to [stormsnuitje@gmail.com](mailto:stormsnuitje@gmail.com) +- Sending a mail to [joshuaslui0203@gmail.com](mailto:joshuaslui0203@gmail.com) - Creating a [security advisory](https://github.com/BlueAtomic/pingernos/security/advisories/new) ... ***Do not report security vulnerabilities in discord, this is highly unsafe and will have consequences on your stay there.*** diff --git a/cogs/blacklist.py b/cogs/blacklist.py index c68d103..b1c59c3 100644 --- a/cogs/blacklist.py +++ b/cogs/blacklist.py @@ -2,7 +2,7 @@ from discord import slash_command, option from discord.ext import commands from discord.ext.bridge import Bot -from utils import Utils +from utilities.database import modifier class Blacklist(commands.Cog): @@ -15,13 +15,7 @@ def __init__(self, bot: Bot): @option("reason", str, description="The reason for the blacklist") async def blacklist(self, ctx, server, reason): """ Blacklist a server from the bot """ - cursor = await Utils.mysql_login() - database = cursor.cursor() - database.execute("INSERT IGNORE INTO blacklist (guild_id, reason) VALUES (%s, %s)", (server.id, reason)) - cursor.commit() - database.close() - cursor.close() - + await modifier("INSERT IGNORE INTO blacklist (guild_id, reason) VALUES (%s, %s)", [server.id, reason]) guild = self.bot.get_guild(server.id) await guild.leave() return await ctx.respond(f'Successfully added guild {server} to the blacklist for:\n**{reason}**') diff --git a/cogs/checkip.py b/cogs/checkip.py index 4d2b944..4ebafef 100644 --- a/cogs/checkip.py +++ b/cogs/checkip.py @@ -1,42 +1,35 @@ -from discord.ext import commands, bridge -from discord import Embed -from mcstatus import JavaServer -from discord.ext.bridge import Bot -from discord.ext.bridge.context import BridgeContext -from utils import Utils +from asyncio import wait_for +from discord import slash_command, option, Embed +from discord.ext import commands +from discord.ext.commands import Bot +from utilities.data import Colors, get_server_status +from utilities.utility import check_ip + class CheckIP(commands.Cog): def __init__(self, bot: Bot): self.bot = bot - @bridge.bridge_command(aliases=["checkserverip", "check"], description="Checks if an Aternos-IP is free to use.") - async def checkip(self, ctx: BridgeContext, address=None): - if address is None: - return await Utils.respond(ctx, "Please provide a Aternos server ip!\nExample: example.aternos.me") - if not address.endswith(".aternos.me"): - address += ".aternos.me" - if address.count(".") > 2: - return await Utils.respond(ctx, "Please provide a valid Aternos server ip!\nExample: example.aternos.me") + @slash_command(aliases=["checkserverip", "check"], description="Checks if an Aternos-IP is free to use.") + @option("address", str, description="The Aternos-IP to check") + async def checkip(self, ctx, address): + address = check_ip(address) + if not address: + return await ctx.respond("Please provide a valid Aternos IP.", ephemeral=True) nip = address.split(".")[0] - if len(nip) > 20: - return await Utils.respond(ctx, - "Aternos IPs can only be 20 characters long, please try a shorter one. Yours is " + str( - len(nip)) + " characters long.") - if len(nip) < 4: - return await Utils.respond(ctx, - "Aternos IPs must be at least 4 characters long, please try a longer one. Yours is " + str( - len(nip)) + " characters long.") + if len(nip) > 20 or len(nip) < 4: + return await ctx.respond(f"Aternos IPs must contain between 4 to 20 characters. You have {len(nip)}/20 characters.", ephemeral=True) await ctx.defer() embed = Embed() - server = await JavaServer.async_lookup(address) - stat = await server.async_status() + stat = await wait_for(get_server_status(address), timeout=2) if stat.version.name == "⚠ Error": embed.description = f"**{address}** is free to use!\nTo use it as your server address, head to **[the options of your server](https://aternos.org/options)**" - embed.colour = Utils.Colors.green + embed.colour = Colors.green else: embed.description = f"**{address}** is already taken!" - embed.colour = Utils.Colors.red - await Utils.respond(ctx=ctx, embed=embed) + embed.colour = Colors.red + await ctx.respond(embed=embed, ephemeral=True) + def setup(bot: Bot): bot.add_cog(CheckIP(bot)) diff --git a/cogs/cogs.py b/cogs/cogs.py index cddadcf..1a3bc95 100644 --- a/cogs/cogs.py +++ b/cogs/cogs.py @@ -3,15 +3,15 @@ from discord.ext import commands from discord import Option from discord.ext.bridge import Bot -from discord.ext.bridge.context import BridgeContext -from utils import Utils +from utilities.data import get_data + class Cogs(commands.Cog): def __init__(self, bot: Bot): self.bot = bot - self.info = Utils.get_data() + self.info = get_data() - def getcogs(self, ctx: BridgeContext) -> list: + def getcogs(self, ctx) -> list: if ctx.interaction.user.id not in self.info['Owners']: return ["You are not an owner of the bot!"] cogs = [] @@ -20,16 +20,15 @@ def getcogs(self, ctx: BridgeContext) -> list: cogs.append(file[:-3]) return cogs - @slash_command(description='Only the owners of the bot can run this command', - guild_ids=Utils.get_data()['FeatureGuilds']) - async def cogs(self, ctx: BridgeContext, action: Option(choices=["Load", "Unload", "Reload"]), cog: Option(autocomplete=getcogs)): + @slash_command(description='Only the owners of the bot can run this command', guild_ids=get_data()['FeatureGuilds']) + async def cogs(self, ctx, action: Option(choices=["Load", "Unload", "Reload"]), cog: Option(autocomplete=getcogs)): if ctx.author.id not in self.info['Owners']: return if cog.lower() not in [f"{fn[:-3]}" for fn in listdir("./cogs")]: - await Utils.respond(ctx, "That cog doesn't exist!") + await ctx.respond("That cog doesn't exist!") return if action.lower() not in ["load", "unload", "reload"]: - await Utils.respond(ctx, "That action doesn't exist!") + await ctx.respond("That action doesn't exist!") return await ctx.defer() try: @@ -40,14 +39,15 @@ async def cogs(self, ctx: BridgeContext, action: Option(choices=["Load", "Unload elif action == "Reload": self.bot.reload_extension(f"cogs.{cog}") except Exception as error: - await Utils.respond(ctx, f"An error has occured!\n{error}") + await ctx.respond(f"An error has occurred!\n{error}") raise error try: await self.bot.sync_commands() except Exception as error: - await Utils.respond(ctx, f"An error has occured!\n{error}") + await ctx.respond(f"An error has occurred!\n{error}") raise error - await Utils.respond(ctx, f"{action}ed {cog} and reloaded all commands!") + await ctx.respond(f"{action}ed {cog} and reloaded all commands!") + def setup(bot: Bot): bot.add_cog(Cogs(bot)) diff --git a/cogs/error.py b/cogs/error.py index 9e45d0a..8435a46 100644 --- a/cogs/error.py +++ b/cogs/error.py @@ -1,6 +1,6 @@ from discord.ext import commands from discord.ext.bridge import Bot -from utils import Utils + class Error(commands.Cog): def __init__(self, bot: Bot): @@ -9,10 +9,10 @@ def __init__(self, bot: Bot): @commands.Cog.listener() async def on_command_error(self, ctx, error): if isinstance(error, commands.CommandNotFound): - return await Utils.respond(ctx, "That command doesn't exist!") + return await ctx.respond("That command doesn't exist!") if isinstance(error, commands.MissingPermissions): - return await Utils.respond(ctx, "You need the `Manage Server` permission to use this command.") - await Utils.respond(ctx, "An unknown error has occured!\nThis has been logged") + return await ctx.respond("You need the `Manage Server` permission to use this command.") + await ctx.respond("An unknown error has occurred!\nThis has been logged") raise error @commands.Cog.listener() @@ -21,8 +21,9 @@ async def on_application_command_error(self, ctx, error): return await ctx.respond("This command is for owners only.") if isinstance(error, commands.GuildNotFound): return await ctx.respond("Could not find this guild.") - await Utils.respond(ctx, "An unknown error has occured!\nThis has been logged") + await ctx.respond("An unknown error has occurred!\nThis has been logged") raise error + def setup(bot: Bot): bot.add_cog(Error(bot)) diff --git a/cogs/info.py b/cogs/info.py index 8b6e4fe..d22eabe 100644 --- a/cogs/info.py +++ b/cogs/info.py @@ -1,18 +1,19 @@ from asyncio import wait_for -from discord.ext import commands, bridge -from discord.ext.bridge import Bot -from discord import Embed -from utils import Utils +from discord import slash_command, Embed +from discord.ext import commands +from discord.ext.commands import Bot +from utilities.data import Colors, get_server_status + class Info(commands.Cog): def __init__(self, bot: Bot): self.bot = bot - @bridge.bridge_command(aliases=["information", "ping", "latency", "pong", "servers", "guilds", "support", "invite"], description = "Displays information about Pingernos") - async def info(self, ctx: bridge.BridgeContext): + @slash_command(aliases=["information", "ping", "latency", "pong", "servers", "guilds", "support", "invite"], description="Displays information about Pingernos") + async def info(self, ctx): embed = Embed() try: - stat = await wait_for(Utils.get_server_status("example.aternos.me"), timeout=2) + stat = await wait_for(get_server_status("example.aternos.me"), timeout=2) except TimeoutError: latency = "N/A" else: @@ -29,8 +30,9 @@ async def info(self, ctx: bridge.BridgeContext): [[Invite]](https://discord.com/api/oauth2/authorize?client_id=889197952994791434&permissions=274878286912&scope=bot%20applications.commands) [[Support]](https://discord.gg/Ukr89GrMBk) [[Github]](https://github.com/BlackFurORG/pingernos) [[Privacy Policy]](https://gist.github.com/MiataBoy/20fda9024f277ea5eb2421adbebc2f23) [[Terms of Service]](https://gist.github.com/MiataBoy/81e96023a2aa055a038edab02e7e7792) """ - embed.colour = Utils.Colors.blue - await Utils.respond(ctx=ctx, embed=embed) + embed.colour = Colors.blue + await ctx.respond(embed=embed) + -def setup(bot: bridge.Bot): +def setup(bot: Bot): bot.add_cog(Info(bot)) diff --git a/cogs/internallogs.py b/cogs/internallogs.py index 2ec6c53..9fcdb49 100644 --- a/cogs/internallogs.py +++ b/cogs/internallogs.py @@ -2,54 +2,50 @@ from discord.ext import commands from discord import Guild, Embed, Webhook from discord.ext.bridge import Bot -from utils import Utils +from utilities.database import selector +from utilities.data import Colors, get_data -class InteralLogs(commands.Cog): + +class InternalLogs(commands.Cog): def __init__(self, bot: Bot): self.bot = bot @commands.Cog.listener() async def on_guild_join(self, guild: Guild): - cursor = await Utils.mysql_login() - database = cursor.cursor() - database.execute("SELECT * FROM blacklist WHERE guild_id = %s", [guild.id]) - - try: - result = database.fetchone()[0] - print(f'Guild {guild} attempted to add {self.bot.user.name}, but was blacklisted.') - embed = Embed(title="Joined a guild!", color=Utils.Colors.red) - embed.add_field(name="Name", value=guild.name, inline=True) - embed.add_field(name="ID", value=guild.id, inline=True) - embed.description(f'Guild attempted to add {self.bot.user.name}, but is blacklisted:\n**{result["reason"]}**') - embed.set_thumbnail(url=guild.icon.url) - async with aiohttp.ClientSession() as client_session: - webhook = Webhook.from_url(Utils.get_data()['Logs']["JoinWebhook"], session=client_session) - await webhook.send(embed=embed, username="Pingernos Logs", avatar_url=self.bot.user.avatar.url) + result = (await selector("SELECT * FROM blacklist WHERE guild_id = %s", [guild.id]))[1] + + if result: await guild.leave() - except TypeError: - pass - embed = Embed(title="Joined a guild!", color=Utils.Colors.green) + embed = Embed(title="Joined a guild!", color=Colors.green) embed.add_field(name="Name", value=guild.name, inline=True) embed.add_field(name="ID", value=guild.id, inline=True) embed.add_field(name="Guild member count", value=guild.member_count, inline=True) embed.add_field(name="Current server count", value=len(self.bot.guilds), inline=True) - embed.set_thumbnail(url=guild.icon.url) + if result: + embed.description = f'**THIS GUILD IS BLACKLISTED**\n**Reason:** {result}' + if guild.icon is not None: + embed.set_thumbnail(url=guild.icon) async with aiohttp.ClientSession() as client_session: - webhook = Webhook.from_url(Utils.get_data()['Logs']["JoinWebhook"], session=client_session) + webhook = Webhook.from_url(get_data()['Logs']["JoinWebhook"], session=client_session) await webhook.send(embed=embed, username="Pingernos Logs", avatar_url=self.bot.user.avatar.url) @commands.Cog.listener() async def on_guild_remove(self, guild: Guild): - embed = Embed(title="Left a guild!", color=Utils.Colors.red) + result = (await selector("SELECT * FROM blacklist WHERE guild_id = %s", [guild.id]))[1] + embed = Embed(title="Left a guild!", color=Colors.red) embed.add_field(name="Name", value=guild.name, inline=True) embed.add_field(name="ID", value=guild.id, inline=True) embed.add_field(name="Guild member count", value=guild.member_count, inline=True) embed.add_field(name="Current server count", value=len(self.bot.guilds), inline=True) - embed.set_thumbnail(url=guild.icon.url) + if result: + embed.description = f'**THIS GUILD IS BLACKLISTED**\n**Reason:** {result}' + if guild.icon is not None: + embed.set_thumbnail(url=guild.icon) async with aiohttp.ClientSession() as client_session: - webhook = Webhook.from_url(Utils.get_data()['Logs']["LeaveWebhook"], session=client_session) + webhook = Webhook.from_url(get_data()['Logs']["LeaveWebhook"], session=client_session) await webhook.send(embed=embed, username="Pingernos Logs", avatar_url=self.bot.user.avatar.url) + def setup(bot: Bot): - bot.add_cog(InteralLogs(bot)) + bot.add_cog(InternalLogs(bot)) diff --git a/cogs/setserver.py b/cogs/setserver.py index 32c9f4f..e69ef2a 100644 --- a/cogs/setserver.py +++ b/cogs/setserver.py @@ -1,31 +1,27 @@ -from discord.ext import commands, bridge -from discord.ext.bridge import Bot -from utils import Utils +from discord import slash_command +from discord.ext import commands +from discord.ext.commands import Bot +from utilities.database import modifier +from utilities.utility import check_ip + class SetServer(commands.Cog): def __init__(self, bot: Bot): self.bot = bot - @bridge.bridge_command(aliases=["set"], - description="Set the default server to use if no argument is provided in the status command.") - @bridge.has_permissions(manage_guild=True) + @slash_command(aliases=["set"], description="Set the default server to use if no argument is provided in the status command.") + @commands.has_permissions(manage_guild=True) async def setserver(self, ctx, server=None): if server is None: - return await Utils.respond(ctx, - "Please provide a server IP to register to this guild. If an IP is already registered, it'll be overwritten") - if not server.endswith(".aternos.me"): - server += ".aternos.me" - if server.count(".") > 2: - return await Utils.respond(ctx, "Please provide a valid Aternos server ip!\nExample: example.aternos.me") - cursor = await Utils.mysql_login() - database = cursor.cursor() - database.execute( - "INSERT INTO server (guild_id, server_ip) VALUES (%s, %s) ON DUPLICATE KEY UPDATE server_ip = %s", - (ctx.guild_id, server, server)) - cursor.commit() - database.close() - cursor.close() - await Utils.respond(ctx, f'The IP has been set to {server}. Use `status` without an argument to view it.') + await modifier("DELETE FROM server WHERE guild_id = %s", [ctx.guild_id]) + return await ctx.respond("Default server has been removed. Use `setserver ` to set a new one.", ephemeral=True) + server = check_ip(server) + if not server: + return await ctx.respond("Please provide a valid Aternos IP.", ephemeral=True) + + await modifier("INSERT INTO server (guild_id, server_ip) VALUES (%s, %s) ON DUPLICATE KEY UPDATE server_ip = %s", [ctx.guild_id, server, server]) + await ctx.respond(f'The IP has been set to {server}. Use `status` without an argument to view it.' , ephemeral=True) + -def setup(bot: bridge.Bot): +def setup(bot: Bot): bot.add_cog(SetServer(bot)) diff --git a/cogs/status.py b/cogs/status.py index 93dcba4..3fc8d52 100644 --- a/cogs/status.py +++ b/cogs/status.py @@ -1,59 +1,50 @@ from asyncio import wait_for -from discord.ext import commands, bridge -from discord.ext.bridge import Bot -from discord import Embed, utils as dutils -from utils import Utils +from discord import slash_command, option, Embed, utils as dutils +from discord.ext import commands +from discord.ext.commands import Bot +from utilities.data import remove_colors_from_string, Colors, get_server_status +from utilities.database import selector +from utilities.utility import check_ip + class Status(commands.Cog): def __init__(self, bot: Bot): self.bot = bot - @bridge.bridge_command(aliases=["s"], description="Get the server status") + @slash_command(aliases=["s"], description="Get the server status") + @option("serverip", str, description="The Aternos-IP to check") async def status(self, ctx, serverip=None): if serverip is None: - cursor = await Utils.mysql_login() - database = cursor.cursor() - database.execute("SELECT server_ip FROM server WHERE guild_id = %s", [ctx.guild.id]) - try: - result = database.fetchone()[0] - except TypeError: - database.close() - cursor.close() - return await Utils.respond(ctx, "Sorry, but this server does not have an IP registered. Please use `setserver` for that.") - serverip = result - database.close() - cursor.close() - if not serverip.endswith(".aternos.me"): - serverip += ".aternos.me" - if serverip.count(".") > 2: - return await Utils.respond(ctx, "Please provide a valid Aternos server ip!\nExample: example.aternos.me") - if serverip.count(":") == 1: - if len(serverip.split(":")[1]) != 5: - return await Utils.respond(ctx, "Please provide a valid Aternos server ip!\nExample: example.aternos.me") + serverip = (await selector('SELECT server_ip FROM server WHERE guild_id = %s', [ctx.guild.id]))[0] + if not serverip: + return await ctx.respond("Sorry, but this server does not have an IP registered. Please use `setserver` for that.", ephemeral=True) + serverip = check_ip(serverip) + if not serverip: + return await ctx.respond("Please provide a valid Aternos IP.", ephemeral=True) await ctx.defer() try: - stat = await wait_for(Utils.get_server_status(serverip), timeout=3) + stat = await wait_for(get_server_status(serverip), timeout=3) except TimeoutError: - return await Utils.respond(ctx, "Uh oh! The protocol took too long to respond! This will likely fix itself.") + return await ctx.respond("Uh oh! The protocol took too long to respond! This will likely fix itself.", ephemeral=True) embed = Embed(title=serverip) if stat.version.name == "§4● Offline": embed.description = "We are not able to gather info from offline servers, sorry!\nProtocol Latency: " + str( round( stat.latency)) + "ms\n\nIf you believe this is wrong, please [join our discord server](https://discord.gg/G2AaJbvdHT)." - embed.colour = Utils.Colors.red + embed.colour = Colors.red embed.timestamp = dutils.utcnow() embed.set_footer(text="Command executed by " + ctx.author.name + "#" + ctx.author.discriminator) elif stat.version.name == "⚠ Error": embed.description = "Server does not exist\nProtocol Latency: " + str(round( stat.latency)) + "ms\n\nIf you believe this is wrong, please [join our discord server](https://discord.gg/G2AaJbvdHT)." - embed.colour = Utils.Colors.red + embed.colour = Colors.red embed.timestamp = dutils.utcnow() embed.set_footer(text="Command executed by " + ctx.author.name + "#" + ctx.author.discriminator) elif stat.version.name == "§4● Starting": embed.description = "We are not able to gather info from starting servers, sorry!\nProtocol Latency: " + str( round( stat.latency)) + "ms\n\nIf you believe this is wrong, please [join our discord server](https://discord.gg/G2AaJbvdHT)." - embed.colour = Utils.Colors.red + embed.colour = Colors.red embed.timestamp = dutils.utcnow() embed.set_footer(text="Command executed by " + ctx.author.name + "#" + ctx.author.discriminator) else: @@ -61,11 +52,11 @@ async def status(self, ctx, serverip=None): embed.add_field(name="**__Players__**", value=str(stat.players.online) + "/" + str(stat.players.max), inline=True) embed.add_field(name="**__Software__**", value=stat.version.name, inline=True) - embed.add_field(name="**__MOTD__**", value=Utils.remove_colors_from_string(stat.description), inline=False) - embed.colour = Utils.Colors.green + embed.add_field(name="**__MOTD__**", value=remove_colors_from_string(stat.description), inline=False) + embed.colour = Colors.green embed.timestamp = dutils.utcnow() embed.set_footer(text="Command executed by " + ctx.author.name + "#" + ctx.author.discriminator) - await Utils.respond(ctx, embed=embed) + await ctx.respond(embed=embed) def setup(bot: Bot): diff --git a/main.py b/main.py index 266487d..201bcd2 100644 --- a/main.py +++ b/main.py @@ -1,32 +1,33 @@ from discord import Intents, Status, Activity, ActivityType from discord.ext.bridge import AutoShardedBot -from utils import Utils +from utilities.database import mysql_login +from utilities.data import get_data -data = Utils.get_data() +data = get_data() intents = Intents(guilds=True, guild_messages=True) -# intents.message_content = True #Uncomment this if you use prefixed command that are not mentions -bot = AutoShardedBot(intents=intents, command_prefix=data['Prefix'], status=Status.dnd, - activity=Activity(type=ActivityType.watching, name="Starting...")) +bot = AutoShardedBot(intents=intents, status=Status.dnd, activity=Activity(type=ActivityType.watching, name="Starting...")) bot.load_extensions("cogs") # Loads all cogs in the cogs folder -bot.help_command = Utils.HelpCmd() # Disables the default help command BOOTED = False + @bot.listen() async def on_connect(): print('Connected to Discord!') - cursor = await Utils.mysql_login() + cursor = await mysql_login() database = cursor.cursor() database.execute("CREATE TABLE IF NOT EXISTS server (guild_id VARCHAR(255) PRIMARY KEY, server_ip TEXT NOT NULL)") database.execute("CREATE TABLE IF NOT EXISTS blacklist (guild_id VARCHAR(21) PRIMARY KEY, reason TEXT NOT NULL)") database.close() + @bot.listen() async def on_reconnect(): print('Reconnected to Discord!') + @bot.listen() async def on_ready(): - global BOOTED #I'm sorry, but there's no other way to do this without classes which I want only in the cogs + global BOOTED # I'm sorry, but there's no other way to do this without classes which I want only in the cogs if BOOTED: print("Reconnect(?)") if not BOOTED: @@ -34,7 +35,7 @@ async def on_ready(): print(f'Logged in as {bot.user} with {bot.shard_count+1} shards!') print('------') for shard in bot.shards: - await bot.change_presence(status=Status.dnd, activity=Activity(type=ActivityType.watching, name=f"you (prefix: @mention) | Shard: {shard+1}"), shard_id=shard) + await bot.change_presence(status=Status.online, activity=Activity(type=ActivityType.watching, name=f"Aternos | Shard: {shard+1}"), shard_id=shard) BOOTED = True bot.run(data['Token']) diff --git a/utilities/data.py b/utilities/data.py new file mode 100644 index 0000000..8cb4cdd --- /dev/null +++ b/utilities/data.py @@ -0,0 +1,81 @@ +from re import sub +from json import load, decoder +from os import getenv +from sys import exit as sysexit + +try: + from dotenv import load_dotenv + load_dotenv() +except ModuleNotFoundError: + print('You did not install the dotenv module! You will not be able to use a .env file.') +try: + from mcstatus import JavaServer + from mcstatus.pinger import PingResponse +except ModuleNotFoundError: + print('You did not install the mcstatus module! Exiting now...') + sysexit() + + +class Colors: + blue = 0xadd8e6 + red = 0xf04747 + green = 0x90ee90 + orange = 0xfaa61a + + +def remove_colors_from_string(text) -> str: + text = sub(r"§[0-9a-r]", "", text) + return text + + +def get_data() -> dict: + """ + This function is used to get the data from the config.json file. + If you do not have a config.json file, you can use environment variables. + :return: The data from the config.json file. + """ + usejson = False # Set to True to a config.json + if usejson: + try: + with open('config.json', 'r', encoding="UTF-8") as file: + data = load(file) + except FileNotFoundError: + print('config.json not found! Exiting now...') + sysexit() + except decoder.JSONDecodeError: + print('config.json is not valid! Exiting now...') + sysexit() + if not usejson: + try: + data = { + "Token": getenv('TOKEN'), + "Prefix": getenv('PREFIX'), + "Owners": getenv('OWNERS').split(','), + "FeatureGuilds": getenv('FEATURE_GUILDS').split(','), + "Database": { + "Host": getenv('DB_HOST'), + "User": getenv('DB_USER'), + "Password": getenv('DB_PASSWORD'), + "Database": getenv('DB_DATABASE'), + "Port": getenv('DB_PORT') + }, + "Logs": { + "JoinWebhook": getenv('LOGS_JOINWEBHOOK'), + "LeaveWebhook": getenv('LOGS_LEAVEWEBHOOK') + } + } + except AttributeError: + print('You did not fill out the environment variables! Exiting now...') + sysexit() + return data + + +async def get_server_status(serverip: str) -> PingResponse: + """ + This function is used to get the status of a server. + :param serverip: The ip of the server. + :return: The status of the server. + """ + server = await JavaServer.async_lookup(serverip) + stat = await server.async_status() + return stat diff --git a/utilities/database.py b/utilities/database.py new file mode 100644 index 0000000..84631cc --- /dev/null +++ b/utilities/database.py @@ -0,0 +1,50 @@ +from mysql import connector as mysql +from utilities.data import get_data + + +async def mysql_login(): + """ + This function is used to log in to the database. + :return: The cursor to use for queries. + """ + database = get_data()['Database'] + + return mysql.connect( + host=database['Host'], + user=database['User'], + password=database['Password'], + database=database['Database']) + + +async def selector(query: str, variables: list): + """ + This function is used to select data from the database. It is used for SELECT queries. + :param query: The query to execute. Use %s for variables. Example: "SELECT * FROM table WHERE column = %s" + :param variables: The variables to use in the query. If there are no variables, use an empty list. + :return: The result of the query. If there is no result, it will return False. + """ + cursor = await mysql_login() + database = cursor.cursor() + database.execute(query, variables) + try: + result = database.fetchall()[0] + except IndexError: + return [False] + database.close() + cursor.close() + return result + + +async def modifier(query: str, variables: list) -> None: + """ + This function is used to modify data in the database. It is used for INSERT, UPDATE, and DELETE queries. + :param query: The query to execute. Use %s for variables. Example: "INSERT INTO table (column) VALUES (%s)" + :param variables: The variables to use in the query. If there are no variables, use an empty list. + :return: None + """ + cursor = await mysql_login() + database = cursor.cursor() + database.execute(query, variables) + cursor.commit() + database.close() + cursor.close() diff --git a/utilities/utility.py b/utilities/utility.py new file mode 100644 index 0000000..bbf6173 --- /dev/null +++ b/utilities/utility.py @@ -0,0 +1,11 @@ +import re + + +def check_ip(server: str) -> str: + if not server.endswith('.aternos.me'): + server += '.aternos.me' + regex = re.compile(r"\w+\.aternos\.me") + matcher = re.findall(regex, server) + if not matcher: + return False + return matcher[0] diff --git a/utils.py b/utils.py deleted file mode 100644 index 2e741a8..0000000 --- a/utils.py +++ /dev/null @@ -1,112 +0,0 @@ -from re import sub -from json import load, decoder -from os import getenv -from sys import exit as sysexit -from discord.ext.commands import HelpCommand -from discord.ext.bridge.context import BridgeContext, BridgeExtContext, BridgeApplicationContext -from discord import Embed -import mysql.connector as mysql - -try: - from dotenv import load_dotenv - load_dotenv() -except ModuleNotFoundError: - print('You did not install the dotenv module! You will not be able to use a .env file.') -try: - from mcstatus import JavaServer - from mcstatus.pinger import PingResponse -except ModuleNotFoundError: - print('You did not install the mcstatus module! Exiting now...') - sysexit() - - -class Utils: - @staticmethod # This is a static method, you can call it without creating an instance of the class, but does not have access to the class or its attributes (self) - def remove_colors_from_string(text) -> str: - text = sub(r"§[0-9a-r]", "", text) - return text - - class Colors: - blue = 0xadd8e6 - red = 0xf04747 - green = 0x90ee90 - orange = 0xfaa61a - - @staticmethod - def get_data() -> dict: - usejson = True # Set to True to a config.json - if usejson: - try: - with open('config.json', 'r', encoding="UTF-8") as file: - data = load(file) - except FileNotFoundError: - print('config.json not found! Exiting now...') - sysexit() - except decoder.JSONDecodeError: - print('config.json is not valid! Exiting now...') - sysexit() - if not usejson: - try: - data = { - "Token": getenv('TOKEN'), - "Prefix": getenv('PREFIX'), - "Owners": getenv('OWNERS').split(','), - "FeatureGuilds": getenv('FEATURE_GUILDS').split(','), - "Database": { - "Host": getenv('DB_HOST'), - "User": getenv('DB_USER'), - "Password": getenv('DB_PASSWORD'), - "Database": getenv('DB_DATABASE'), - "Port": getenv('DB_PORT') - }, - "Logs": { - "JoinWebhook": getenv('LOGS_JOINWEBHOOK'), - "LeaveWebhook": getenv('LOGS_LEAVEWEBHOOK') - } - } - except AttributeError: - print('You did not fill out the environment variables! Exiting now...') - sysexit() - return data - - @staticmethod - async def get_server_status(serverip: str) -> PingResponse: - server = await JavaServer.async_lookup(serverip) - stat = await server.async_status() - return stat - - @staticmethod - async def mysql_login(): - data = Utils.get_data() - - return mysql.connect( - host=data['Database']['Host'], - user=data['Database']['User'], - password=data['Database']['Password'], - database=data['Database']['Database']) - - class HelpCmd(HelpCommand): - async def send_bot_help(self, mapping): - channel = self.get_destination() - await channel.send("Type in `/` to see the commands!", reference=self.context.message, mention_author=False, - delete_after=15) - - @staticmethod - async def respond(ctx: BridgeContext, message: str="", embed: Embed=None) -> None: - if isinstance(ctx, BridgeApplicationContext): - if embed is not None: - await ctx.respond(message, embed=embed) - return - await ctx.respond(message) - return - if isinstance(ctx, BridgeExtContext): - if embed is not None: - await ctx.respond(message, embed=embed, mention_author=False) - return - await ctx.respond(message, mention_author=False) - return - if embed is not None: - await ctx.respond(message, embed=embed) - return - await ctx.respond(message) - return