basic bot setup
This commit is contained in:
parent
a5682eb260
commit
afef3ae674
16
.vscode/launch.json
vendored
Normal file
16
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Python Debugger: Current File",
|
||||||
|
"type": "debugpy",
|
||||||
|
"request": "launch",
|
||||||
|
"program": "${workspaceFolder}/bot.py",
|
||||||
|
"console": "integratedTerminal",
|
||||||
|
"justMyCode": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
113
bot.py
Normal file
113
bot.py
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
"""
|
||||||
|
Spiffo
|
||||||
|
A Discord Bot for integration with a Project Zomboid game server.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import atexit
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import logging.config
|
||||||
|
from os import getenv
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from discord import Intents
|
||||||
|
from discord.ext import commands
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
load_dotenv(override=True)
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent
|
||||||
|
|
||||||
|
|
||||||
|
class DiscordBot(commands.Bot):
|
||||||
|
"""
|
||||||
|
Represents a Discord bot.
|
||||||
|
Contains controls to interact with the bot via the Discord API.
|
||||||
|
"""
|
||||||
|
rcon_details: dict
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(command_prefix="-", intents=Intents.all())
|
||||||
|
self.rcon_details = {
|
||||||
|
"host": getenv("SPIFFO__RCON_HOST"),
|
||||||
|
"port": getenv("SPIFFO__RCON_PORT"),
|
||||||
|
"passwd": getenv("SPIFFO__RCON_PASSWORD")
|
||||||
|
}
|
||||||
|
|
||||||
|
async def sync_app_commands(self):
|
||||||
|
"""
|
||||||
|
Sync application commands between Discord and the bot.
|
||||||
|
"""
|
||||||
|
await self.wait_until_ready()
|
||||||
|
await self.tree.sync()
|
||||||
|
log.info("Application commands successfully synced")
|
||||||
|
|
||||||
|
async def on_ready(self):
|
||||||
|
"""
|
||||||
|
Execute init operations that require the bot to be ready.
|
||||||
|
Ideally should not be manually called, this is handled by discord.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
await self.sync_app_commands()
|
||||||
|
log.info("Discord Bot is ready")
|
||||||
|
|
||||||
|
async def load_cogs(self):
|
||||||
|
"""
|
||||||
|
Load any extensions found in the cogs dictionary.
|
||||||
|
"""
|
||||||
|
|
||||||
|
for path in (BASE_DIR / "cogs").iterdir():
|
||||||
|
if path.suffix == ".py":
|
||||||
|
await self.load_extension(f"cogs.{path.stem}")
|
||||||
|
|
||||||
|
|
||||||
|
def get_bot_token() -> str:
|
||||||
|
"""
|
||||||
|
Retrieve the access token of the bot from the environment.
|
||||||
|
Raises `ValueError` if token is missing or blank.
|
||||||
|
"""
|
||||||
|
bot_token = getenv("SPIFFO__BOT_TOKEN")
|
||||||
|
if not bot_token:
|
||||||
|
raise ValueError("'SPIFFO__BOT_TOKEN' environment variable is missing or empty.")
|
||||||
|
|
||||||
|
return bot_token
|
||||||
|
|
||||||
|
def setup_logging_config():
|
||||||
|
"""
|
||||||
|
Loads the logging configuration and creates an asynchronous queue handler.
|
||||||
|
"""
|
||||||
|
log_config_path = BASE_DIR / "logging.json"
|
||||||
|
if not log_config_path.exists():
|
||||||
|
raise FileNotFoundError(log_config_path)
|
||||||
|
|
||||||
|
with open(log_config_path, "r", encoding="utf-8") as file:
|
||||||
|
log_config = json.load(file)
|
||||||
|
|
||||||
|
# Ensure the logging directory exists
|
||||||
|
(BASE_DIR / "logs").mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
# Load the config
|
||||||
|
logging.config.dictConfig(log_config) # This 'logging.config' path is jank.
|
||||||
|
|
||||||
|
# Register the queue handler for asynchronous logging
|
||||||
|
queue_handler = logging.getHandlerByName("queue_handler")
|
||||||
|
if queue_handler is not None:
|
||||||
|
queue_handler.listener.start()
|
||||||
|
atexit.register(queue_handler.listener.stop)
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
"""
|
||||||
|
The entrypoint function, initialises the application.
|
||||||
|
"""
|
||||||
|
setup_logging_config()
|
||||||
|
bot_token = get_bot_token()
|
||||||
|
|
||||||
|
async with DiscordBot() as bot:
|
||||||
|
await bot.load_cogs()
|
||||||
|
await bot.start(bot_token, reconnect=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
115
cogs/commands.py
Normal file
115
cogs/commands.py
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
"""
|
||||||
|
Contains commands for users to influence the bot via Discord.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from rcon.source import rcon
|
||||||
|
from discord.ext import commands
|
||||||
|
from discord import app_commands, Interaction, Permissions
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
COMMANDS_BLACKLIST = [
|
||||||
|
"additem",
|
||||||
|
"addvehicle",
|
||||||
|
"addxp",
|
||||||
|
"alarm",
|
||||||
|
"chopper",
|
||||||
|
"clear",
|
||||||
|
"createhorde",
|
||||||
|
"createhorde2",
|
||||||
|
"godmod",
|
||||||
|
"gunshot",
|
||||||
|
"invisible",
|
||||||
|
"lightning",
|
||||||
|
"log",
|
||||||
|
"noclip",
|
||||||
|
"quit",
|
||||||
|
"releasesafehouse",
|
||||||
|
"removezombies",
|
||||||
|
"replay",
|
||||||
|
"setaccesslevel",
|
||||||
|
"startrain",
|
||||||
|
"startstorm",
|
||||||
|
"stoprain",
|
||||||
|
"stopweather",
|
||||||
|
"teleport",
|
||||||
|
"teleportto",
|
||||||
|
"thunder"
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class CommandCog(commands.Cog):
|
||||||
|
"""
|
||||||
|
Contains user commands.
|
||||||
|
"""
|
||||||
|
def __init__(self, bot: commands.Bot):
|
||||||
|
self.bot = bot
|
||||||
|
|
||||||
|
@commands.Cog.listener()
|
||||||
|
async def on_ready(self):
|
||||||
|
log.info("%s cog is ready", self.__class__.__name__)
|
||||||
|
|
||||||
|
rcon_group = app_commands.Group(
|
||||||
|
name="rcon",
|
||||||
|
description="Remote console commands",
|
||||||
|
default_permissions=Permissions.all(),
|
||||||
|
guild_only=True
|
||||||
|
)
|
||||||
|
|
||||||
|
async def send_rcon_response(self, inter: Interaction, command: str) -> str:
|
||||||
|
# if inter.user.id != 377453890523627522:
|
||||||
|
# log.warning("Bad user tried to send rcon command: '%s', '%s'", inter.user.name, command)
|
||||||
|
# await inter.response.send_message("Permissions Error", ephemeral=True)
|
||||||
|
# return
|
||||||
|
|
||||||
|
if command in COMMANDS_BLACKLIST:
|
||||||
|
log.warning("Attempt to use banned command: '%s', '%s'", inter.user.name, command)
|
||||||
|
await inter.response.send_message("Blacklisted command", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
await inter.response.defer()
|
||||||
|
response = await rcon(command, **self.bot.rcon_details)
|
||||||
|
await inter.followup.send(content=response)
|
||||||
|
|
||||||
|
@rcon_group.command(name="command")
|
||||||
|
async def send_rcon_command(self, inter: Interaction, command: str):
|
||||||
|
"""
|
||||||
|
Send a command via remote console.
|
||||||
|
"""
|
||||||
|
await self.send_rcon_response(inter, command)
|
||||||
|
|
||||||
|
@rcon_group.command(name="save")
|
||||||
|
async def send_rcon_save(self, inter: Interaction):
|
||||||
|
"""
|
||||||
|
Save the in-game world.
|
||||||
|
"""
|
||||||
|
await self.send_rcon_response(inter, "save")
|
||||||
|
|
||||||
|
@rcon_group.command(name="servermsg")
|
||||||
|
async def send_rcon_servermsg(self, inter: Interaction, message: str):
|
||||||
|
"""
|
||||||
|
Broadcast a message to all players.
|
||||||
|
"""
|
||||||
|
await self.send_rcon_response(inter, f'servermsg "{message}"')
|
||||||
|
|
||||||
|
@rcon_group.command(name="players")
|
||||||
|
async def send_rcon_players(self, inter: Interaction):
|
||||||
|
"""
|
||||||
|
Get a list of online players.
|
||||||
|
"""
|
||||||
|
await self.send_rcon_response(inter, "players")
|
||||||
|
|
||||||
|
@rcon_group.command(name="check-mods-need-update")
|
||||||
|
async def send_rcon_check_mod_needs_update(self, inter: Interaction):
|
||||||
|
"""
|
||||||
|
Get a list of mods that need an update, if any.
|
||||||
|
"""
|
||||||
|
await self.send_rcon_response(inter, "checkModsNeedUpdate")
|
||||||
|
|
||||||
|
|
||||||
|
async def setup(bot: commands.Bot):
|
||||||
|
cog = CommandCog(bot)
|
||||||
|
await bot.add_cog(cog)
|
||||||
|
log.info("Added %s cog", cog.__class__.__name__)
|
51
logging.json
Normal file
51
logging.json
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"disable_existing_loggers": true,
|
||||||
|
"formatters": {
|
||||||
|
"simple": {
|
||||||
|
"format": "%(levelname)s %(message)s"
|
||||||
|
},
|
||||||
|
"detail": {
|
||||||
|
"format": "[%(asctime)s] [%(levelname)s|%(name)s]: %(message)s"
|
||||||
|
},
|
||||||
|
"complex": {
|
||||||
|
"format": "[%(levelname)s|%(module)s|L%(lineno)d] %(asctime)s %(message)s",
|
||||||
|
"datefmt": "%Y-%m-%dT%H:%M:%S%z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"handlers": {
|
||||||
|
"stdout": {
|
||||||
|
"class": "logging.StreamHandler",
|
||||||
|
"level": "DEBUG",
|
||||||
|
"formatter": "detail",
|
||||||
|
"stream": "ext://sys.stdout"
|
||||||
|
},
|
||||||
|
"file": {
|
||||||
|
"class": "logging.handlers.RotatingFileHandler",
|
||||||
|
"level": "DEBUG",
|
||||||
|
"formatter": "complex",
|
||||||
|
"filename": "logs/spiffo.log",
|
||||||
|
"maxBytes": 1048576,
|
||||||
|
"backupCount": 3
|
||||||
|
},
|
||||||
|
"queue_handler": {
|
||||||
|
"class": "logging.handlers.QueueHandler",
|
||||||
|
"handlers": [
|
||||||
|
"stdout",
|
||||||
|
"file"
|
||||||
|
],
|
||||||
|
"respect_handler_level": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"loggers": {
|
||||||
|
"root": {
|
||||||
|
"level": "DEBUG",
|
||||||
|
"handlers": [
|
||||||
|
"queue_handler"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"discord": {
|
||||||
|
"level": "INFO"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
12
requirements.txt
Normal file
12
requirements.txt
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
aiohappyeyeballs==2.4.4
|
||||||
|
aiohttp==3.11.9
|
||||||
|
aiosignal==1.3.1
|
||||||
|
attrs==24.2.0
|
||||||
|
discord.py==2.4.0
|
||||||
|
frozenlist==1.5.0
|
||||||
|
idna==3.10
|
||||||
|
multidict==6.1.0
|
||||||
|
propcache==0.2.1
|
||||||
|
python-dotenv==1.0.1
|
||||||
|
rcon==2.4.9
|
||||||
|
yarl==1.18.3
|
Reference in New Issue
Block a user