Merge branch 'main' into aurora-hybrid

This commit is contained in:
Seaswimmer 2024-03-07 20:00:09 +00:00
commit 354b505e6e
Signed by: CoastalCommitsManagement
GPG key ID: 7E73189F651A553F
17 changed files with 228 additions and 84 deletions

View file

@ -10,7 +10,7 @@ Aurora is a fully-featured moderation system. It is heavily inspired by Galactic
## Installation ## Installation
```bash ```bash
[p]repo add seacogs https://coastalcommits.com/SeaswimmerTheFsh/SeaCogs [p]repo add seacogs https://www.coastalcommits.com/SeaswimmerTheFsh/SeaCogs
[p]cog install seacogs aurora [p]cog install seacogs aurora
[p]cog load aurora [p]cog load aurora
``` ```

View file

@ -5,7 +5,7 @@ Backup allows you to export a JSON list of all of your installed repositories an
## Installation ## Installation
```bash ```bash
[p]repo add seacogs https://coastalcommits.com/SeaswimmerTheFsh/SeaCogs [p]repo add seacogs https://www.coastalcommits.com/SeaswimmerTheFsh/SeaCogs
[p]cog install seacogs backup [p]cog install seacogs backup
[p]cog load backup [p]cog load backup
``` ```

View file

@ -6,7 +6,7 @@ This cog does require an api key to work.
## Installation ## Installation
```bash ```bash
[p]repo add seacogs https://coastalcommits.com/SeaswimmerTheFsh/SeaCogs [p]repo add seacogs https://www.coastalcommits.com/SeaswimmerTheFsh/SeaCogs
[p]cog install seacogs bible [p]cog install seacogs bible
[p]cog load bible [p]cog load bible
``` ```

View file

@ -5,7 +5,7 @@ Nerdify allows you to nerdify other people's text.
## Installation ## Installation
```bash ```bash
[p]repo add seacogs https://coastalcommits.com/SeaswimmerTheFsh/SeaCogs [p]repo add seacogs https://www.coastalcommits.com/SeaswimmerTheFsh/SeaCogs
[p]cog install seacogs nerdify [p]cog install seacogs nerdify
[p]cog load nerdify [p]cog load nerdify
``` ```

View file

@ -31,11 +31,12 @@ Available placeholders:
- `.$M` - replaced with message content - `.$M` - replaced with message content
- `.$N` - replaced with author's display name (or guild nickname, if set) - `.$N` - replaced with author's display name (or guild nickname, if set)
- `.$U` - replaced with the author's username (NOT display name, you should usually use `.$N`) - `.$U` - replaced with the author's username (NOT display name, you should usually use `.$N`)
- `.$V` - replaced with the configured invite link
Default value: Default value:
```json ```json
tellraw @a ["",{"text":".$N ","color":".$C"},{"text":" (DISCORD): ","color":"blue"},{"text":".$M","color":"white"}] tellraw @a ["",{"text":".$N ","color":".$C","insertion":"<@.$I>","hoverEvent":{"action":"show_text","contents":"Shift click to mention this user inside Discord"}},{"text":"(DISCORD):","color":"blue","clickEvent":{"action":"open_url","value":".$V"},"hoverEvent":{"action":"show_text","contents":"Click to join the Discord Server"}},{"text":" .$M","color":"white"}]
``` ```
## `consolechannel` ## `consolechannel`
@ -62,6 +63,12 @@ This is to prevent the console channel from flooding and getting backed up by Di
Default value: `None` Default value: `None`
## `invite`
This option determines what url the chat command will substitute in for the Discord invite placeholder.
Default value: `None`
## `ip` ## `ip`
This option determines whether or not IP's will be redacted when posted in chat or to the console channel. This option determines whether or not IP's will be redacted when posted in chat or to the console channel.

View file

@ -10,7 +10,7 @@ Pterodactyl allows for connecting to a Pterodactyl server through websockets. It
## Installation ## Installation
```bash ```bash
[p]repo add seacogs https://coastalcommits.com/SeaswimmerTheFsh/SeaCogs [p]repo add seacogs https://www.coastalcommits.com/SeaswimmerTheFsh/SeaCogs
[p]cog install seacogs pterodactyl [p]cog install seacogs pterodactyl
[p]cog load aurora [p]cog load aurora
``` ```

View file

@ -19,7 +19,8 @@ from pytimeparse2 import disable_dateutil, parse
from redbot.core import app_commands, commands, data_manager from redbot.core import app_commands, commands, data_manager
from redbot.core.app_commands import Choice from redbot.core.app_commands import Choice
from redbot.core.bot import Red from redbot.core.bot import Red
from redbot.core.utils.chat_formatting import box, error, warning from redbot.core.utils.chat_formatting import (box, error, humanize_list,
warning)
from aurora.importers.aurora import ImportAuroraView from aurora.importers.aurora import ImportAuroraView
from aurora.importers.galacticbot import ImportGalacticBotView from aurora.importers.galacticbot import ImportGalacticBotView
@ -46,8 +47,8 @@ class Aurora(commands.Cog):
It is heavily inspired by GalacticBot, and is designed to be a more user-friendly alternative to Red's core Mod cogs. It is heavily inspired by GalacticBot, and is designed to be a more user-friendly alternative to Red's core Mod cogs.
This cog stores all of its data in an SQLite database.""" This cog stores all of its data in an SQLite database."""
__author__ = "SeaswimmerTheFsh" __author__ = ["SeaswimmerTheFsh"]
__version__ = "2.0.5" __version__ = "2.0.6"
async def red_delete_data_for_user(self, *, requester, user_id: int): async def red_delete_data_for_user(self, *, requester, user_id: int):
if requester == "discord_deleted_user": if requester == "discord_deleted_user":
@ -86,6 +87,16 @@ class Aurora(commands.Cog):
disable_dateutil() disable_dateutil()
self.handle_expiry.start() self.handle_expiry.start()
def format_help_for_context(self, ctx: commands.Context) -> str:
pre_processed = super().format_help_for_context(ctx) or ""
n = "\n" if "\n\n" not in pre_processed else ""
text = [
f"{pre_processed}{n}",
f"Cog Version: **{self.__version__}**",
f"Author: {humanize_list(self.__author__)}",
]
return "\n".join(text)
async def cog_load(self): async def cog_load(self):
"""This method prepares the database schema for all of the guilds the bot is currently in.""" """This method prepares the database schema for all of the guilds the bot is currently in."""
guilds: list[discord.Guild] = self.bot.guilds guilds: list[discord.Guild] = self.bot.guilds

View file

@ -1,3 +1,3 @@
import logging from red_commons.logging import getLogger
logger = logging.getLogger("red.sea.aurora") logger = getLogger("red.seacogs.aurora")

View file

@ -7,28 +7,38 @@
import contextlib import contextlib
import json import json
import logging
import re import re
from red_commons.logging import getLogger
from redbot.cogs.downloader import errors from redbot.cogs.downloader import errors
from redbot.cogs.downloader.converters import InstalledCog from redbot.cogs.downloader.converters import InstalledCog
from redbot.core import commands from redbot.core import commands
from redbot.core.bot import Red from redbot.core.bot import Red
from redbot.core.utils.chat_formatting import error, text_to_file from redbot.core.utils.chat_formatting import (error, humanize_list,
text_to_file)
# pylint: disable=protected-access # pylint: disable=protected-access
class Backup(commands.Cog): class Backup(commands.Cog):
"""A utility to make reinstalling repositories and cogs after migrating the bot far easier.""" """A utility to make reinstalling repositories and cogs after migrating the bot far easier."""
__author__ = "SeaswimmerTheFsh" __author__ = ["SeaswimmerTheFsh"]
__version__ = "1.0.0" __version__ = "1.0.1"
def __init__(self, bot: Red): def __init__(self, bot: Red):
super().__init__() super().__init__()
self.bot = bot self.bot = bot
self.logger = logging.getLogger("red.sea.backup") self.logger = getLogger("red.seacogs.backup")
def format_help_for_context(self, ctx: commands.Context) -> str:
pre_processed = super().format_help_for_context(ctx) or ""
n = "\n" if "\n\n" not in pre_processed else ""
text = [
f"{pre_processed}{n}",
f"Cog Version: **{self.__version__}**",
f"Author: {humanize_list(self.__author__)}",
]
return "\n".join(text)
@commands.group(autohelp=True) @commands.group(autohelp=True)
@commands.is_owner() @commands.is_owner()
@ -86,6 +96,9 @@ class Backup(commands.Cog):
"""Import your installed repositories and cogs from an export file.""" """Import your installed repositories and cogs from an export file."""
try: try:
export = json.loads(await ctx.message.attachments[0].read()) export = json.loads(await ctx.message.attachments[0].read())
except (json.JSONDecodeError, IndexError):
try:
export = json.loads(await ctx.message.reference.resolved.attachments[0].read())
except (json.JSONDecodeError, IndexError): except (json.JSONDecodeError, IndexError):
await ctx.send(error("Please provide a valid JSON export file.")) await ctx.send(error("Please provide a valid JSON export file."))
return return

View file

@ -7,9 +7,9 @@
"end_user_data_statement" : "This cog does not store end user data.", "end_user_data_statement" : "This cog does not store end user data.",
"hidden": false, "hidden": false,
"disabled": false, "disabled": false,
"min_bot_version": "3.5.5", "min_bot_version": "3.5.0",
"max_bot_version": "3.5.5", "max_bot_version": "3.5.5",
"min_python_version": [3, 10, 0], "min_python_version": [3, 9, 0],
"tags": [ "tags": [
"utility", "utility",
"backup", "backup",

View file

@ -5,14 +5,14 @@
# ____) | __/ (_| \__ \\ V V /| | | | | | | | | | | | __/ | # ____) | __/ (_| \__ \\ V V /| | | | | | | | | | | | __/ |
# |_____/ \___|\__,_|___/ \_/\_/ |_|_| |_| |_|_| |_| |_|\___|_| # |_____/ \___|\__,_|___/ \_/\_/ |_|_| |_| |_|_| |_| |_|\___|_|
import logging
import random import random
import aiohttp import aiohttp
from discord import Embed from discord import Embed
from red_commons.logging import getLogger
from redbot.core import Config, commands from redbot.core import Config, commands
from redbot.core.bot import Red from redbot.core.bot import Red
from redbot.core.utils.chat_formatting import error from redbot.core.utils.chat_formatting import error, humanize_list
import bible.errors import bible.errors
from bible.models import Version from bible.models import Version
@ -21,8 +21,8 @@ from bible.models import Version
class Bible(commands.Cog): class Bible(commands.Cog):
"""Retrieve Bible verses from the API.bible API.""" """Retrieve Bible verses from the API.bible API."""
__author__ = "SeaswimmerTheFsh" __author__ = ["SeaswimmerTheFsh"]
__version__ = "1.0.0" __version__ = "1.0.1"
def __init__(self, bot: Red): def __init__(self, bot: Red):
super().__init__() super().__init__()
@ -31,10 +31,20 @@ class Bible(commands.Cog):
self.config = Config.get_conf( self.config = Config.get_conf(
self, identifier=481923957134912, force_registration=True self, identifier=481923957134912, force_registration=True
) )
self.logger = logging.getLogger("red.sea.bible") self.logger = getLogger("red.seacogs.bible")
self.config.register_global(bible="de4e12af7f28f599-02") self.config.register_global(bible="de4e12af7f28f599-02")
self.config.register_user(bible=None) self.config.register_user(bible=None)
def format_help_for_context(self, ctx: commands.Context) -> str:
pre_processed = super().format_help_for_context(ctx) or ""
n = "\n" if "\n\n" not in pre_processed else ""
text = [
f"{pre_processed}{n}",
f"Cog Version: **{self.__version__}**",
f"Author: {humanize_list(self.__author__)}",
]
return "\n".join(text)
async def translate_book_name(self, bible_id: str, book_name: str) -> str: async def translate_book_name(self, bible_id: str, book_name: str) -> str:
"""Translate a book name to a book ID.""" """Translate a book name to a book ID."""
book_name_list = [ book_name_list = [

View file

@ -17,12 +17,22 @@ from redbot.core.utils import chat_formatting, common_filters
class Nerdify(commands.Cog): class Nerdify(commands.Cog):
"""Nerdify your text.""" """Nerdify your text."""
__author__ = "SeaswimmerTheFsh" __author__ = ["SeaswimmerTheFsh"]
__version__ = "1.3.2" __version__ = "1.3.3"
def __init__(self, bot): def __init__(self, bot):
self.bot = bot self.bot = bot
def format_help_for_context(self, ctx: commands.Context) -> str:
pre_processed = super().format_help_for_context(ctx) or ""
n = "\n" if "\n\n" not in pre_processed else ""
text = [
f"{pre_processed}{n}",
f"Cog Version: **{self.__version__}**",
f"Author: {chat_formatting.humanize_list(self.__author__)}",
]
return "\n".join(text)
@commands.command(aliases=["nerd"]) @commands.command(aliases=["nerd"])
async def nerdify( async def nerdify(
self, ctx: commands.Context, *, text: Optional[str] = None self, ctx: commands.Context, *, text: Optional[str] = None

View file

@ -13,7 +13,7 @@ def register_config(config_obj: Config) -> None:
join_regex=r"^\[\d{2}:\d{2}:\d{2} INFO\]: ([^<\n]+) joined the game$", join_regex=r"^\[\d{2}:\d{2}:\d{2} INFO\]: ([^<\n]+) joined the game$",
leave_regex=r"^\[\d{2}:\d{2}:\d{2} INFO\]: ([^<\n]+) left the game$", leave_regex=r"^\[\d{2}:\d{2}:\d{2} INFO\]: ([^<\n]+) left the game$",
achievement_regex=r"^\[\d{2}:\d{2}:\d{2} INFO\]: (.*) has (made the advancement|completed the challenge) \[(.*)\]$", achievement_regex=r"^\[\d{2}:\d{2}:\d{2} INFO\]: (.*) has (made the advancement|completed the challenge) \[(.*)\]$",
chat_command='tellraw @a ["",{"text":".$N ","color":".$C"},{"text":" (DISCORD): ","color":"blue"},{"text":".$M","color":"white"}]', chat_command='tellraw @a ["",{"text":".$N ","color":".$C","insertion":"<@.$I>","hoverEvent":{"action":"show_text","contents":"Shift click to mention this user inside Discord"}},{"text":"(DISCORD):","color":"blue","clickEvent":{"action":"open_url","value":".$V"},"hoverEvent":{"action":"show_text","contents":"Click to join the Discord Server"}},{"text":" .$M","color":"white"}]', # noqa: E501
api_endpoint="minecraft", api_endpoint="minecraft",
chat_channel=None, chat_channel=None,
startup_msg='Server started!', startup_msg='Server started!',
@ -21,5 +21,6 @@ def register_config(config_obj: Config) -> None:
join_msg='Welcome to the server! 👋', join_msg='Welcome to the server! 👋',
leave_msg='Goodbye! 👋', leave_msg='Goodbye! 👋',
mask_ip=True, mask_ip=True,
invite=None,
regex_blacklist={}, regex_blacklist={},
) )

View file

@ -9,7 +9,7 @@
"disabled": false, "disabled": false,
"min_bot_version": "3.5.0", "min_bot_version": "3.5.0",
"min_python_version": [3, 8, 0], "min_python_version": [3, 8, 0],
"requirements": ["py-dactyl", "websockets"], "requirements": ["git+https://github.com/SeaswimmerTheFsh/pydactyl", "websockets"],
"tags": [ "tags": [
"pterodactyl", "pterodactyl",
"minecraft", "minecraft",

View file

@ -1,3 +1,4 @@
import logging from red_commons.logging import getLogger
logger = logging.getLogger('red.sea.pterodactyl') logger = getLogger('red.seacogs.pterodactyl')
websocket_logger = getLogger('red.seacogs.pterodactyl.websocket')

View file

@ -1,13 +1,14 @@
import asyncio import asyncio
import json import json
from typing import Mapping, Optional from typing import Mapping, Optional, Union
import discord import discord
import websockets import websockets
from pydactyl import PterodactylClient from pydactyl import PterodactylClient
from redbot.core import commands from redbot.core import app_commands, commands
from redbot.core.app_commands import Choice
from redbot.core.bot import Red from redbot.core.bot import Red
from redbot.core.utils.chat_formatting import box from redbot.core.utils.chat_formatting import box, error
from redbot.core.utils.views import ConfirmView from redbot.core.utils.views import ConfirmView
from pterodactyl.config import config, register_config from pterodactyl.config import config, register_config
@ -91,11 +92,85 @@ class Pterodactyl(commands.Cog):
"M": message.content.replace('"',''), "M": message.content.replace('"',''),
"N": message.author.display_name, "N": message.author.display_name,
"U": message.author.name, "U": message.author.name,
"V": await config.invite() or "use [p]pterodactyl config invite to change me",
} }
for key, value in placeholders.items(): for key, value in placeholders.items():
command = command.replace('.$' + key, value) command = command.replace('.$' + key, value)
return command return command
async def power(self, ctx: Union[discord.Interaction, commands.Context], action: str, action_ing: str, warning: str = '') -> None:
if isinstance(ctx, discord.Interaction):
author = ctx.user
else:
author = ctx.author
current_status = await config.current_status()
if current_status == action_ing:
if isinstance(ctx, discord.Interaction):
return await ctx.response.send_message(f"Server is already {action_ing}.", ephemeral=True)
return await ctx.send(f"Server is already {action_ing}.")
if current_status in ["starting", "stopping"]:
if isinstance(ctx, discord.Interaction):
return await ctx.response.send_message("Another power action is already in progress.", ephemeral=True)
return await ctx.send("Another power action is already in progress.")
view = ConfirmView(author, disable_buttons=True)
if isinstance(ctx, discord.Interaction):
await ctx.response.send_message(f"{warning}Are you sure you want to {action} the server?", view=view)
else:
message = await ctx.send(f"{warning}Are you sure you want to {action} the server?", view=view)
await view.wait()
if view.result is True:
if isinstance(ctx, discord.Interaction):
await ctx.edit_original_response(content=f"Sending websocket command to {action} server...", view=None)
else:
await message.edit(content=f"Sending websocket command to {action} server...", view=None)
await self.websocket.send(json.dumps({"event": "set state", "args": [action]}))
if isinstance(ctx, discord.Interaction):
await ctx.edit_original_response(content=f"Server {action_ing}", view=None)
else:
await message.edit(content=f"Server {action_ing}", view=None)
else:
if isinstance(ctx, discord.Interaction):
await ctx.edit_original_response(content="Cancelled.", view=None)
else:
await message.edit(content="Cancelled.", view=None)
async def send_command(self, ctx: Union[discord.Interaction, commands.Context], command: str):
channel = self.bot.get_channel(await config.console_channel())
if isinstance(ctx, discord.Interaction):
if channel:
await channel.send(f"Received console command from {ctx.user.id}: {command[:1900]}")
try:
await self.websocket.send(json.dumps({"event": "send command", "args": [command]}))
await ctx.response.send_message(f"Command sent to server. {box(command, 'json')}", ephemeral=True)
except websockets.exceptions.ConnectionClosed as e:
logger.error("WebSocket connection closed: %s", e)
await ctx.response.send_message(error("WebSocket connection closed."))
self.task.cancel()
self.retry_counter = 0
self.task = self.get_task()
else:
if channel:
await channel.send(f"Received console command from {ctx.author.id}: {command[:1900]}")
try:
await self.websocket.send(json.dumps({"event": "send command", "args": [command]}))
await ctx.send(f"Command sent to server. {box(command, 'json')}")
except websockets.exceptions.ConnectionClosed as e:
logger.error("WebSocket connection closed: %s", e)
await ctx.send(error("WebSocket connection closed."))
self.task.cancel()
self.retry_counter = 0
self.task = self.get_task()
@commands.Cog.listener() @commands.Cog.listener()
async def on_red_api_tokens_update(self, service_name: str, api_tokens: Mapping[str,str]): # pylint: disable=unused-argument async def on_red_api_tokens_update(self, service_name: str, api_tokens: Mapping[str,str]): # pylint: disable=unused-argument
if service_name == "pterodactyl": if service_name == "pterodactyl":
@ -104,48 +179,70 @@ class Pterodactyl(commands.Cog):
self.retry_counter = 0 self.retry_counter = 0
self.task = self.get_task() self.task = self.get_task()
slash_pterodactyl = app_commands.Group(name="pterodactyl", description="Pterodactyl allows you to manage your Pterodactyl Panel from Discord.")
@slash_pterodactyl.command(name = "command", description = "Send a command to the server console.")
async def slash_pterodactyl_command(self, interaction: discord.Interaction, command: str) -> None:
"""Send a command to the server console.
Parameters:
-----------
command: str
The command to send to the server."""
return await self.send_command(interaction, command)
@slash_pterodactyl.command(name = "power", description = "Send power actions to the server.")
@app_commands.choices(action=[
Choice(name="Start", value="start"),
Choice(name="Stop", value="stop"),
Choice(name="Restart", value="restart"),
Choice(name="⚠️ Kill ⚠️", value="kill")
])
async def slash_pterodactyl_power(self, interaction: discord.Interaction, action: app_commands.Choice[str]) -> None:
"""Send power actions to the server.
Parameters:
-----------
action: app_commands.Choice[str]
The action to perform on the server."""
if action.value == "kill":
return await self.power(interaction, action.value, "stopping... (forcefully killed)", warning="**⚠️ Forcefully killing the server process can corrupt data in some cases. ⚠️**\n")
return await self.power(interaction, action.value, f"{action.value}ing...")
@commands.group(autohelp = True, name = "pterodactyl", aliases = ["ptero"]) @commands.group(autohelp = True, name = "pterodactyl", aliases = ["ptero"])
async def pterodactyl(self, ctx: commands.Context) -> None: async def pterodactyl(self, ctx: commands.Context) -> None:
"""Pterodactyl allows you to manage your Pterodactyl Panel from Discord.""" """Pterodactyl allows you to manage your Pterodactyl Panel from Discord."""
@pterodactyl.command(name = "command", aliases = ["cmd", "execute", "exec"])
@commands.admin()
async def pterodactyl_command(self, ctx: commands.Context, *, command: str) -> None:
"""Send a command to the server console."""
return await self.send_command(ctx, command)
@pterodactyl.group(autohelp = True, name = "power") @pterodactyl.group(autohelp = True, name = "power")
@commands.admin() @commands.admin()
async def pterodactyl_power(self, ctx: commands.Context) -> None: async def pterodactyl_power(self, ctx: commands.Context) -> None:
"""Send power actions to the server.""" """Send power actions to the server."""
@pterodactyl_power.command(name = "start") @pterodactyl_power.command(name = "start")
async def pterodactyl_power_start(self, ctx: commands.Context) -> None: async def pterodactyl_power_start(self, ctx: commands.Context) -> Optional[discord.Message]:
"""Start the server.""" """Start the server."""
current_status = await config.current_status() return await self.power(ctx, "start", "starting...")
if current_status == "running":
return await ctx.send("Server is already running.")
if current_status in ["starting", "stopping"]:
return await ctx.send("Another power action is already in progress.")
message = await ctx.send("Sending websocket command to start server...")
await self.websocket.send(json.dumps({"event": "set state", "args": ["start"]}))
await message.edit(content="Server starting...")
@pterodactyl_power.command(name = "stop") @pterodactyl_power.command(name = "stop")
async def pterodactyl_power_stop(self, ctx: commands.Context) -> None: async def pterodactyl_power_stop(self, ctx: commands.Context) -> Optional[discord.Message]:
"""Stop the server.""" """Stop the server."""
current_status = await config.current_status() return await self.power(ctx, "stop", "stopping...")
if current_status == "stopped":
return await ctx.send("Server is already stopped.")
if current_status in ["starting", "stopping"]:
return await ctx.send("Another power action is already in progress.")
message = await ctx.send("Sending websocket command to stop server...")
await self.websocket.send(json.dumps({"event": "set state", "args": ["stop"]}))
await message.edit(content="Server stopping...")
@pterodactyl_power.command(name = "restart") @pterodactyl_power.command(name = "restart")
async def pterodactyl_power_restart(self, ctx: commands.Context) -> None: async def pterodactyl_power_restart(self, ctx: commands.Context) -> Optional[discord.Message]:
"""Restart the server.""" """Restart the server."""
current_status = await config.current_status() return await self.power(ctx, "restart", "restarting...")
if current_status in ["starting", "stopping"]:
return await ctx.send("Another power action is already in progress.") @pterodactyl_power.command(name = "kill")
message = await ctx.send("Sending websocket command to restart server...") async def pterodactyl_power_kill(self, ctx: commands.Context) -> Optional[discord.Message]:
await self.websocket.send(json.dumps({"event": "set state", "args": ["restart"]})) """Kill the server."""
await message.edit(content="Server restarting...") return await self.power(ctx, "kill", "stopping... (forcefully killed)", warning="**⚠️ Forcefully killing the server process can corrupt data in some cases. ⚠️**\n")
@pterodactyl.group(autohelp = True, name = "config", aliases = ["settings", "set"]) @pterodactyl.group(autohelp = True, name = "config", aliases = ["settings", "set"])
@commands.is_owner() @commands.is_owner()
@ -181,6 +278,12 @@ class Pterodactyl(commands.Cog):
await config.console_channel.set(channel.id) await config.console_channel.set(channel.id)
await ctx.send(f"Console channel set to {channel.mention}") await ctx.send(f"Console channel set to {channel.mention}")
@pterodactyl_config.command(name = "invite")
async def pterodactyl_config_invite(self, ctx: commands.Context, invite: str) -> None:
"""Set the invite link for your server."""
await config.invite.set(invite)
await ctx.send(f"Invite link set to {invite}")
@pterodactyl_config.group(name = "chat") @pterodactyl_config.group(name = "chat")
async def pterodactyl_config_chat(self, ctx: commands.Context): async def pterodactyl_config_chat(self, ctx: commands.Context):
"""Configure chat settings.""" """Configure chat settings."""
@ -287,7 +390,7 @@ class Pterodactyl(commands.Cog):
await config.api_endpoint.set(endpoint) await config.api_endpoint.set(endpoint)
await ctx.send(f"API endpoint set to {endpoint}") await ctx.send(f"API endpoint set to {endpoint}")
@pterodactyl_config_regex.group(name = "blacklist", aliases = ['block', 'blocklist']) @pterodactyl_config_regex.group(name = "blacklist", aliases = ['block', 'blocklist'],)
async def pterodactyl_config_regex_blacklist(self, ctx: commands.Context): async def pterodactyl_config_regex_blacklist(self, ctx: commands.Context):
"""Blacklist regex patterns.""" """Blacklist regex patterns."""
@ -345,6 +448,7 @@ class Pterodactyl(commands.Cog):
leave_msg = await config.leave_msg() leave_msg = await config.leave_msg()
mask_ip = await config.mask_ip() mask_ip = await config.mask_ip()
api_endpoint = await config.api_endpoint() api_endpoint = await config.api_endpoint()
invite = await config.invite()
regex_blacklist: dict = await config.regex_blacklist() regex_blacklist: dict = await config.regex_blacklist()
embed = discord.Embed(color = await ctx.embed_color(), title="Pterodactyl Configuration") embed = discord.Embed(color = await ctx.embed_color(), title="Pterodactyl Configuration")
embed.description = f"""**Base URL:** {base_url} embed.description = f"""**Base URL:** {base_url}
@ -357,6 +461,7 @@ class Pterodactyl(commands.Cog):
**Leave Message:** {leave_msg} **Leave Message:** {leave_msg}
**Mask IP:** {self.get_bool_str(mask_ip)} **Mask IP:** {self.get_bool_str(mask_ip)}
**API Endpoint:** `{api_endpoint}` **API Endpoint:** `{api_endpoint}`
**Invite:** {invite}
**Chat Command:** {box(chat_command, 'json')} **Chat Command:** {box(chat_command, 'json')}
**Chat Regex:** {box(chat_regex, 're')} **Chat Regex:** {box(chat_regex, 're')}

View file

@ -1,7 +1,6 @@
# pylint: disable=cyclic-import # pylint: disable=cyclic-import
import json import json
import re import re
from logging import getLogger
from typing import Optional, Union from typing import Optional, Union
import aiohttp import aiohttp
@ -11,7 +10,7 @@ from pydactyl import PterodactylClient
from redbot.core.utils.chat_formatting import bold, pagify from redbot.core.utils.chat_formatting import bold, pagify
from pterodactyl.config import config from pterodactyl.config import config
from pterodactyl.logger import logger from pterodactyl.logger import logger, websocket_logger
from pterodactyl.pterodactyl import Pterodactyl from pterodactyl.pterodactyl import Pterodactyl
@ -23,7 +22,7 @@ async def establish_websocket_connection(coginstance: Pterodactyl) -> None:
websocket_credentials = await retrieve_websocket_credentials(coginstance) websocket_credentials = await retrieve_websocket_credentials(coginstance)
async with websockets.connect(websocket_credentials['data']['socket'], origin=base_url, ping_timeout=60, logger=getLogger("red.sea.pterodactyl.websocket")) as websocket: async with websockets.connect(websocket_credentials['data']['socket'], origin=base_url, ping_timeout=60, logger=websocket_logger) as websocket:
logger.info("WebSocket connection established") logger.info("WebSocket connection established")
auth_message = json.dumps({"event": "auth", "args": [websocket_credentials['data']['token']]}) auth_message = json.dumps({"event": "auth", "args": [websocket_credentials['data']['token']]})
@ -58,7 +57,7 @@ async def establish_websocket_connection(coginstance: Pterodactyl) -> None:
if content.startswith('['): if content.startswith('['):
pagified_content = pagify(content, delims=[" ", "\n"]) pagified_content = pagify(content, delims=[" ", "\n"])
for page in pagified_content: for page in pagified_content:
await channel.send(content=page) await channel.send(content=page, allowed_mentions=discord.AllowedMentions.none())
server_message = await check_if_server_message(content) server_message = await check_if_server_message(content)
if server_message: if server_message:
@ -135,10 +134,7 @@ async def retrieve_websocket_credentials(coginstance: Pterodactyl) -> Optional[d
coginstance.task.cancel() coginstance.task.cancel()
raise ValueError("Pterodactyl server ID not set. Please set it using `[p]pterodactyl config serverid`.") raise ValueError("Pterodactyl server ID not set. Please set it using `[p]pterodactyl config serverid`.")
#FIXME - pydactyl should not be overriding the global python logger, but until that issue is fixed, client = PterodactylClient(base_url, api_key).client
# we need to set the pydactyl logger to debug so it doesn't ignore any non-error log
# relevant issue - https://github.com/iamkubi/pydactyl/issues/82
client = PterodactylClient(base_url, api_key, debug=True).client
coginstance.client = client coginstance.client = client
websocket_credentials = client.servers.get_websocket(server_id) websocket_credentials = client.servers.get_websocket(server_id)
logger.debug("""Websocket connection details retrieved: logger.debug("""Websocket connection details retrieved:
@ -156,48 +152,39 @@ def remove_ansi_escape_codes(text: str) -> str:
return ansi_escape.sub('', text) return ansi_escape.sub('', text)
async def check_if_server_message(text: str) -> Union[bool, str]: async def check_if_server_message(text: str) -> Union[bool, str]:
logger.debug("Checking if message is a server message")
regex = await config.server_regex() regex = await config.server_regex()
match: Optional[re.Match[str]] = re.match(regex, text) match: Optional[re.Match[str]] = re.match(regex, text)
if match: if match:
logger.debug("Message is a server message") logger.debug("Message is a server message")
return match.group(1) return match.group(1)
logger.debug("Message is not a server message")
return False return False
async def check_if_chat_message(text: str) -> Union[bool, dict]: async def check_if_chat_message(text: str) -> Union[bool, dict]:
logger.debug("Checking if message is a chat message")
regex = await config.chat_regex() regex = await config.chat_regex()
match: Optional[re.Match[str]] = re.match(regex, text) match: Optional[re.Match[str]] = re.match(regex, text)
if match: if match:
groups = {"username": match.group(1), "message": match.group(2)} groups = {"username": match.group(1), "message": match.group(2)}
logger.debug("Message is a chat message\n%s", json.dumps(groups)) logger.debug("Message is a chat message\n%s", json.dumps(groups))
return groups return groups
logger.debug("Message is not a chat message")
return False return False
async def check_if_join_message(text: str) -> Union[bool, str]: async def check_if_join_message(text: str) -> Union[bool, str]:
logger.debug("Checking if message is a join message")
regex = await config.join_regex() regex = await config.join_regex()
match: Optional[re.Match[str]] = re.match(regex, text) match: Optional[re.Match[str]] = re.match(regex, text)
if match: if match:
logger.debug("Message is a join message") logger.debug("Message is a join message")
return match.group(1) return match.group(1)
logger.debug("Message is not a join message")
return False return False
async def check_if_leave_message(text: str) -> Union[bool, str]: async def check_if_leave_message(text: str) -> Union[bool, str]:
logger.debug("Checking if message is a leave message")
regex = await config.leave_regex() regex = await config.leave_regex()
match: Optional[re.Match[str]] = re.match(regex, text) match: Optional[re.Match[str]] = re.match(regex, text)
if match: if match:
logger.debug("Message is a leave message") logger.debug("Message is a leave message")
return match.group(1) return match.group(1)
logger.debug("Message is not a leave message")
return False return False
async def check_if_achievement_message(text: str) -> Union[bool, dict]: async def check_if_achievement_message(text: str) -> Union[bool, dict]:
logger.debug("Checking if message is an achievement message")
regex = await config.achievement_regex() regex = await config.achievement_regex()
match: Optional[re.Match[str]] = re.match(regex, text) match: Optional[re.Match[str]] = re.match(regex, text)
if match: if match:
@ -206,9 +193,8 @@ async def check_if_achievement_message(text: str) -> Union[bool, dict]:
groups["challenge"] = True groups["challenge"] = True
else: else:
groups["challenge"] = False groups["challenge"] = False
logger.debug("Message is an achievement message\n%s", json.dumps(groups)) logger.debug("Message is an achievement message")
return groups return groups
logger.debug("Message is not an achievement message")
return False return False
async def get_info(username: str) -> Optional[dict]: async def get_info(username: str) -> Optional[dict]:
@ -217,7 +203,7 @@ async def get_info(username: str) -> Optional[dict]:
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.get(f"https://playerdb.co/api/player/{endpoint}/{username}") as response: async with session.get(f"https://playerdb.co/api/player/{endpoint}/{username}") as response:
if response.status == 200: if response.status == 200:
logger.debug("Player info retrieved for %s\n%s", username, json.dumps(await response.json())) logger.debug("Player info retrieved for %s", username)
return await response.json() return await response.json()
logger.error("Failed to retrieve player info for %s: %s", username, response.status) logger.error("Failed to retrieve player info for %s: %s", username, response.status)
return None return None
@ -230,10 +216,10 @@ async def send_chat_discord(coginstance: Pterodactyl, username: str, message: st
webhook = discord.utils.get(webhooks, name="Pterodactyl Chat") webhook = discord.utils.get(webhooks, name="Pterodactyl Chat")
if webhook is None: if webhook is None:
webhook = await channel.create_webhook(name="Pterodactyl Chat") webhook = await channel.create_webhook(name="Pterodactyl Chat")
await webhook.send(content=message, username=username, avatar_url=avatar_url, allowed_mentions=discord.AllowedMentions.none()) await webhook.send(content=message, username=username, avatar_url=avatar_url, allowed_mentions=discord.AllowedMentions(everyone=False, roles=False, users=True))
logger.debug("Chat message sent to Discord") logger.debug("Chat message sent to Discord")
else: else:
logger.debug("Chat channel not set. Skipping sending chat message to Discord") logger.warning("Chat channel not set. Skipping sending chat message to Discord")
async def generate_join_leave_embed(username: str, join: bool) -> discord.Embed: async def generate_join_leave_embed(username: str, join: bool) -> discord.Embed:
embed = discord.Embed() embed = discord.Embed()
@ -260,7 +246,7 @@ async def generate_achievement_embed(username: str, achievement: str, challenge:
return embed return embed
def mask_ip(string: str) -> str: def mask_ip(string: str) -> str:
def check(match): def check(match: re.Match[str]):
ip = match.group(0) ip = match.group(0)
masked_ip = '.'.join(r'\*' * len(octet) for octet in ip.split('.')) masked_ip = '.'.join(r'\*' * len(octet) for octet in ip.split('.'))
return masked_ip return masked_ip