diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS deleted file mode 100644 index 21d4349..0000000 --- a/.github/CODEOWNERS +++ /dev/null @@ -1,3 +0,0 @@ -* @DeltaSLM @Hilex23 -LICENSE @DeltaSLM -.github/CODEOWNERS @DeltaSLM diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql.yml similarity index 54% rename from .github/workflows/codeql-analysis.yml rename to .github/workflows/codeql.yml index b8ea9fc..02efbc2 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql.yml @@ -13,12 +13,12 @@ name: "CodeQL" on: push: - branches: [ main ] + branches: [ "main" ] pull_request: # The branches below must be a subset of the branches above - branches: [ main ] + branches: [ "main" ] schedule: - - cron: '24 2 * * 1' + - cron: '34 0 * * 2' jobs: analyze: @@ -32,39 +32,45 @@ jobs: strategy: fail-fast: false matrix: - language: [ 'javascript' ] + language: [ 'python' ] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] - # Learn more about CodeQL language support at https://git.io/codeql-language-support + # Use only 'java' to analyze code written in Java, Kotlin or both + # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@v2 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. - # queries: ./path/to/local/query, your-org/your-repo/queries@main - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + + # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v1 + uses: github/codeql-action/autobuild@v2 # โ„น๏ธ Command-line programs to run using the OS shell. - # ๐Ÿ“š https://git.io/JvXDl + # ๐Ÿ“š See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - # โœ๏ธ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. - #- run: | - # make bootstrap - # make release + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@v2 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml new file mode 100644 index 0000000..fd14689 --- /dev/null +++ b/.github/workflows/pylint.yml @@ -0,0 +1,26 @@ +name: Pylint + +on: [push] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10.9", "3.11.1"] + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Install pylint + run: | + python -m pip install --upgrade pip + pip install pylint + - name: Install dependencies + run: | + pip install -r requirements.txt + - name: Analysing the code with pylint + run: | + pylint $(git ls-files '*.py') diff --git a/.gitignore b/.gitignore index 21559c6..ca57c1a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ config.json -node_modules .env -.env.local -.env.development -local.gitignore +.idea +.vscode +cogs/__pycache__ +__pycache__ \ No newline at end of file diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..fd001ce --- /dev/null +++ b/.pylintrc @@ -0,0 +1,2 @@ +[MESSAGES CONTROL] +disable=line-too-long,missing-function-docstring,missing-class-docstring,missing-module-docstring,too-few-public-methods,no-name-in-module,global-statement \ No newline at end of file diff --git a/CONFIGURATION.md b/CONFIGURATION.md new file mode 100644 index 0000000..5409547 --- /dev/null +++ b/CONFIGURATION.md @@ -0,0 +1,66 @@ +# 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 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`, the configuration file uses `PascalCase`. + +If a config file is missing, pingernos will try to load the environment variables. If both are missing, pingernos will exit. + +## 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` | + +## Prefix (required) +Prefix to use for commands, can be a mention or a string + +| 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` | + +## Database (required) +MySQL/MariaDB access credentials. Other SQL dialects or Databases are not supported. + +### Host +Database hostname or IP + +| type | config file | environment | +|--------|-----------------|------------------------| +| string | `Database.Host` | `DB_HOST` | + +### User +Database username + +| type | config file | environment | +|--------|-----------------|------------------------| +| string | `Database.User` | `DB_USER` | + +### Password +Database password + +| type | config file | environment | +|--------|---------------------|----------------------------| +| string | `Database.Password` | `DB_PASSWORD` | + +### Database +Database name + +| 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 diff --git a/README.md b/README.md index 6444765..d710195 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,9 @@ -# Pingernos +# About +This is a fork of pingernos written in py-cord, this is not affiliated with the original pingernos bot or Aternos + +# Original README.md + +## Pingernos Pingernos is a Discord bot that lets you grab the status of any Aternos server. This bot takes **mentions** as prefix. You can use **<@889197952994791434> help** to view a list of the commands. @@ -14,5 +19,5 @@ The API we use is not affiliated with Aternos, Minecraft or any server host. Thi You can invite the bot here: https://discord.com/api/oauth2/authorize?client_id=889197952994791434&permissions=274878286848&scope=bot -## NOTE -Pingernos has been transferred to BlackFur Studios ORG. and will from now on fall under authority and legal notice/compliance of BlackFur Studios ORG. The privacy policy and legal terms of former PsychOps Studios are no longer present and will be rewritten in name of BlackFur Studios. Please bear with us as we attempt to do this as fast as we can. BlackFur Studios ORG. does not have any affiliations with the now-inactive PsychOps Studios and only takes authority over Pingernos and any entities that are directly related to Pingernos. +### NOTE +Pingernos has been transferred to BlackFur Studios ORG. and will from now on fall under authority and legal notice/compliance of BlackFur Studios ORG. The privacy policy and legal terms of former PsychOps Studios are no longer present and will be rewritten in name of BlackFur Studios. Please bear with us as we attempt to do this as fast as we can. BlackFur Studios ORG. does not have any affiliations with the now-inactive PsychOps Studios and only takes authority over Pingernos and any entities that are directly related to Pingernos. \ No newline at end of file diff --git a/cogs/checkip.py b/cogs/checkip.py new file mode 100644 index 0000000..1035aa3 --- /dev/null +++ b/cogs/checkip.py @@ -0,0 +1,40 @@ +#from re import match +from discord.ext import commands, bridge +from discord import Embed +from mcstatus import JavaServer +from utils import Utils +class CheckIP(commands.Cog): + def __init__(self, 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, address = None): + if address is None: + return await ctx.respond("Please provide a Aternos server ip!\nExample: example.aternos.me") + #if not match(r"^(\w+)(?:\.aternos\.me)?$", address): + # return await ctx.respond(f"`{address}`is not a valid Aternos server IP or name.") + if not address.endswith(".aternos.me"): + address += ".aternos.me" + if address.count(".") > 2: + return await ctx.respond("Please provide a valid Aternos server ip!\nExample: example.aternos.me") + nip = address.split(".")[0] + if len(nip) > 20: + return await ctx.respond("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 ctx.respond("Aternos IPs must be at least 4 characters long, please try a longer one. Yours is " + str(len(nip)) + " characters long.") + #async with ctx.typing(): + await ctx.defer() + embed = Embed() + server = await JavaServer.async_lookup(address) + stat = await server.async_status() + 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 + else: + embed.description=f"**{address}** is already taken!" + embed.colour = Utils.Colors.red + await ctx.respond(embed=embed) + + +def setup(bot): + bot.add_cog(CheckIP(bot)) diff --git a/cogs/cogs.py b/cogs/cogs.py new file mode 100644 index 0000000..bbf5034 --- /dev/null +++ b/cogs/cogs.py @@ -0,0 +1,50 @@ +from os import listdir +from discord.ext.commands import slash_command +from discord.ext import commands +from discord import Option +#from discord.errors import ExtensionAlreadyLoaded +from utils import Utils +class Cogs(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.info = Utils.get_data() + + def getcogs(self, ctx): + if ctx.interaction.user.id not in self.info['Owners']: + return ["You are not an owner of the bot!"] + cogs = [] + for file in listdir("./cogs"): + if file.endswith(".py"): + cogs.append(file[:-3]) + return cogs + + @slash_command(description='Only the owners of the bot can run this command', guild_ids=[1041398130677141564]) + 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 ctx.respond("That cog doesn't exist!", ephemeral=True) + return + if action.lower() not in ["load", "unload", "reload"]: + await ctx.respond("That action doesn't exist!", ephemeral=True) + return + await ctx.defer() + try: + if action == "Load": + self.bot.load_extension(f"cogs.{cog}") + elif action == "Unload": + self.bot.unload_extension(f"cogs.{cog}") + elif action == "Reload": + self.bot.reload_extension(f"cogs.{cog}") + except Exception as error: + await ctx.respond(f"An error has occured!\n{error}") + raise error + try: + await self.bot.sync_commands() + except Exception as error: + await ctx.respond(f"An error has occured!\n{error}") + raise error + await ctx.respond(f"{action}ed {cog} and reloaded all commands!") + +def setup(bot): + bot.add_cog(Cogs(bot)) diff --git a/cogs/error.py b/cogs/error.py new file mode 100644 index 0000000..3f9a206 --- /dev/null +++ b/cogs/error.py @@ -0,0 +1,19 @@ +from discord.ext import commands +class Error(commands.Cog): + def __init__(self, bot): + self.bot = bot + + @commands.Cog.listener() + async def on_command_error(self, ctx, error): + if isinstance(error, commands.CommandNotFound): + return await ctx.respond("That command doesn't exist!") + await ctx.respond ("An unknown error has occured!\nThis has been logged") + raise error + + @commands.Cog.listener() + async def on_application_command_error(self, ctx, error): + await ctx.respond ("An unknown error has occured!\nThis has been logged") + raise error + +def setup(bot): + bot.add_cog(Error(bot)) diff --git a/cogs/guilds.py b/cogs/guilds.py new file mode 100644 index 0000000..2dca60f --- /dev/null +++ b/cogs/guilds.py @@ -0,0 +1,16 @@ +from discord.ext import commands, bridge +from discord import Embed +from utils import Utils +class Guilds(commands.Cog): + def __init__(self, bot): + self.bot = bot + + @bridge.bridge_command(aliases=["servers"], description = "Shows how many guilds the bot is in.") + async def guilds(self, ctx): + embed = Embed() + embed.description = f"{self.bot.user.name} is currently in {len(self.bot.guilds)} servers with a total of {sum(x.member_count for x in self.bot.guilds)} users." + embed.color = Utils.Colors.blue + await ctx.respond(embed=embed) + +def setup(bot): + bot.add_cog(Guilds(bot)) diff --git a/cogs/ping.py b/cogs/ping.py new file mode 100644 index 0000000..1dc6e65 --- /dev/null +++ b/cogs/ping.py @@ -0,0 +1,18 @@ +from asyncio import wait_for +from discord.ext import commands, bridge +from utils import Utils +class Ping(commands.Cog): + def __init__(self, bot): + self.bot = bot + + @bridge.bridge_command(aliases=["latency", "pong"], description="Get the bot and websocket latency") + async def ping(self, ctx): + #await ctx.defer() + try: + stat = await wait_for(Utils.get_server_status("example.aternos.me"), timeout=2) + except TimeoutError: + return await ctx.respond(f"Uh oh! The protocol took too long to respond! This will likely fix itself.\nAPI Latency is {round(self.bot.latency*1000, 2)}ms") + await ctx.respond(content=f"API Latency is {round(self.bot.latency*1000, 2)}ms\nProtocol: {round(stat.latency)}ms") + +def setup(bot): + bot.add_cog(Ping(bot)) diff --git a/cogs/privacy.py b/cogs/privacy.py new file mode 100644 index 0000000..9984384 --- /dev/null +++ b/cogs/privacy.py @@ -0,0 +1,16 @@ +from discord.ext import commands, bridge +from discord import Embed +from utils import Utils +class Privacy(commands.Cog): + def __init__(self, bot): + self.bot = bot + + @bridge.bridge_command(description = "Shows the privacy policy of the bot.") + async def privacy(self, ctx): + embed = Embed() + embed.description = f"{self.bot.user.name} saves a minimal amount of data to allow for its functionality. As we work on regaining full functionality, we do not currently collect anything On our database storage, we currently only have server IPs and guild IDs stored. For any concerns, Mail to miataboymx@gmail.com" + embed.colour = Utils.Colors.blue + await ctx.respond(embed=embed) + +def setup(bot): + bot.add_cog(Privacy(bot)) diff --git a/cogs/setserver.py b/cogs/setserver.py new file mode 100644 index 0000000..06e25ed --- /dev/null +++ b/cogs/setserver.py @@ -0,0 +1,14 @@ +from discord.ext import commands#, bridge +class SetServer(commands.Cog): + def __init__(self, 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.") + #async def setserver(self, ctx, server = None): + # if server is None: + # return await ctx.respond("Please provide a server IP.") + # await ctx.defer() + #This is up to Miataboy to implement + +def setup(bot): + bot.add_cog(SetServer(bot)) diff --git a/cogs/status.py b/cogs/status.py new file mode 100644 index 0000000..29d118e --- /dev/null +++ b/cogs/status.py @@ -0,0 +1,45 @@ +from asyncio import wait_for +from discord.ext import commands, bridge +from discord import Embed, utils as dutils +from utils import Utils +class Status(commands.Cog): + def __init__(self, bot): + self.bot = bot + + @bridge.bridge_command(aliases=["s"], description="Get the server status") + async def status(self, ctx, serverip = None): + if serverip is None: + #Until Miataboy implements the default server, this will appear + return await ctx.respond("Please provide a valid Aternos server ip!\nExample: example.aternos.me") + if not serverip.endswith(".aternos.me"): + serverip += ".aternos.me" + if serverip.count(".") > 2: + return await ctx.respond("Please provide a valid Aternos server ip!\nExample: example.aternos.me") + await ctx.defer() + try: + stat = await wait_for(Utils.get_server_status(serverip), timeout=3) + except TimeoutError: + return await ctx.respond("Uh oh! The protocol took too long to respond! This will likely fix itself.") + 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.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.timestamp = dutils.utcnow() + embed.set_footer(text="Command executed by " + ctx.author.name + "#" + ctx.author.discriminator) + else: + embed.add_field(name="**__Status__**", value="Online", inline=True) + 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.timestamp = dutils.utcnow() + embed.set_footer(text="Command executed by " + ctx.author.name + "#" + ctx.author.discriminator) + await ctx.respond(embed=embed) + +def setup(bot): + bot.add_cog(Status(bot)) diff --git a/example.config.json b/example.config.json index 8135360..fd1cb2e 100644 --- a/example.config.json +++ b/example.config.json @@ -1,11 +1,13 @@ { - "token": "your_token_here", - "prefix": "<@!BotID> ", - "owners": ["OwnerID"], - "database": { - "host": "DB host IP", - "user": "DB user", - "password": "DB Password", - "database": "DB name" - } + "Configuration": "See configuration.md", + "Token": "YOURBOTTOKEN", + "Prefix": "YOURBOTPREFIX", + "Owners": [123, 456], + "Database": { + "Host": "DB host IP", + "User": "DB user", + "Password": "DB Password", + "Database": "DB name", + "Port": 3306 + } } \ No newline at end of file diff --git a/example.env b/example.env new file mode 100644 index 0000000..d27896d --- /dev/null +++ b/example.env @@ -0,0 +1,8 @@ +TOKEN=YOURBOTTOKEN +PREFIX=YOURBOTPREFIX +OWNERS="123, 456, 789" +DB_HOST=YOURDATABASEHOST +DB_USER=YOURDATABASEUSER +DB_PASSWORD=YOURDATABASEPASSWORD +DB_DATABASE=YOURDATABASENAME +DB_PORT=YOURDATABASEPORT \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..62180fe --- /dev/null +++ b/main.py @@ -0,0 +1,25 @@ +from discord import Intents, Status, Activity, ActivityType +from discord.ext.bridge import Bot +from utils import Utils +data = Utils.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 = Bot(intents=intents, command_prefix=data['Prefix'], status=Status.dnd, activity=Activity(type=ActivityType.playing, name="you (prefix: @mention)")) +bot.load_extensions("cogs") #Loads all cogs in the cogs folder +BOOTED = False +@bot.listen() +async def on_connect(): + print('Connected to Discord!') + +@bot.listen() +async def on_ready(): + global BOOTED + if not BOOTED: + await bot.sync_commands() + print(f'Logged in as {bot.user}') + print('------') + BOOTED = True + if BOOTED: + print ("Reconnect(?)") + +bot.run(data['Token']) diff --git a/package.json b/package.json deleted file mode 100644 index e4616a7..0000000 --- a/package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "serverpinger", - "version": "1.0.0", - "dependencies": { - "discord.js": "^13.1.0", - "minecraft-protocol": "^1.26.5", - "mysql2": "^2.3.3", - "statcord.js": "^3.3.0" - } -} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..38f5b41 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +py-cord==2.3.2 +python-dotenv==0.21.0 +mcstatus==10.0.1 +asyncmy==0.2.5 \ No newline at end of file diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..73be88a --- /dev/null +++ b/utils.py @@ -0,0 +1,60 @@ +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 +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 = 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() + except EncodingWarning: + print('config.json is not encoded in UTF-8! Exiting now...') + sysexit() + if not usejson: + #If you don't fill out the environment variables, it will return empty and probably crash, so make sure you fill them out! + data = { + "Token": getenv('TOKEN'), + "Prefix": getenv('PREFIX'), + "Owners": getenv('OWNERS').split(','), + "Database": { + "Host": getenv('DB_HOST'), + "User": getenv('DB_USER'), + "Password": getenv('DB_PASSWORD'), + "Database": getenv('DB_DATABASE'), + "Port": getenv('DB_PORT') + } + } + return data + @staticmethod + async def get_server_status(serverip: str) -> dict: + server = await JavaServer.async_lookup(serverip) + stat = await server.async_status() + return stat