WIP: Make Aurora commands hybrid commands #17
17 changed files with 228 additions and 84 deletions
|
@ -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
|
||||||
```
|
```
|
||||||
|
|
|
@ -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
|
||||||
```
|
```
|
||||||
|
|
|
@ -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
|
||||||
```
|
```
|
||||||
|
|
|
@ -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
|
||||||
```
|
```
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
```
|
```
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
import logging
|
from red_commons.logging import getLogger
|
||||||
|
|
||||||
logger = logging.getLogger("red.sea.aurora")
|
logger = getLogger("red.seacogs.aurora")
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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 = [
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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={},
|
||||||
)
|
)
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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')}
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue