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