mirror of
https://github.com/Specoolazius/mcserver-discordbot.git
synced 2025-01-19 00:16:12 +01:00
Compare commits
16 Commits
3af0aaafa4
...
0a26310a2f
Author | SHA1 | Date | |
---|---|---|---|
0a26310a2f | |||
f6cfdc6f28 | |||
39ee845ce1 | |||
42904399e9 | |||
3c835bcf38 | |||
e1bb77c073 | |||
6cf08376da | |||
71a590b9a6 | |||
547a4bfed6 | |||
e8c6125dce | |||
3058754821 | |||
ef3ce4dec6 | |||
670cec5335 | |||
33a0340c3c | |||
9a730f7b2b | |||
e50961bfe0 |
381
bot/botclient.py
381
bot/botclient.py
@ -1,381 +0,0 @@
|
|||||||
import pprint
|
|
||||||
from abc import ABC
|
|
||||||
import asyncio
|
|
||||||
from configparser import ConfigParser
|
|
||||||
from datetime import datetime
|
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
import random
|
|
||||||
import re
|
|
||||||
|
|
||||||
import time
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
import discord
|
|
||||||
from discord.commands import slash_command, SlashCommandGroup
|
|
||||||
from discord.ext import tasks
|
|
||||||
import mcstatus
|
|
||||||
from mcstatus import JavaServer
|
|
||||||
|
|
||||||
CONFIG_PATH = 'config'
|
|
||||||
|
|
||||||
|
|
||||||
class Configs(object):
|
|
||||||
"""< object >
|
|
||||||
|
|
||||||
This contains all configs and settings defined in config.ini and settings.ini
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.start_time = datetime.now()
|
|
||||||
path_list = re.split('/| \\\\', CONFIG_PATH)
|
|
||||||
|
|
||||||
config = ConfigParser(allow_no_value=True)
|
|
||||||
config.read(f'{os.path.join(*path_list, "") or ""}config.ini')
|
|
||||||
|
|
||||||
self.__login_token = config.get('discord', 'token')
|
|
||||||
|
|
||||||
self.server_address = config.get('mcserver', 'server_address')
|
|
||||||
self.server_port = config.getint('mcserver', 'server_port', fallback=25565)
|
|
||||||
|
|
||||||
self.admin_ids = config.get('perms', 'admin_ids')
|
|
||||||
if self.admin_ids:
|
|
||||||
self.admin_ids = [int(admin_id) for admin_id in self.admin_ids.split(' ')]
|
|
||||||
|
|
||||||
self.role_ids = config.get('perms', 'role_ids')
|
|
||||||
if self.admin_ids:
|
|
||||||
self.role_ids = [int(admin_id) for admin_id in self.admin_ids.split(' ')]
|
|
||||||
|
|
||||||
self.executable_commands = {
|
|
||||||
'start': config.getboolean('perms', 'start', fallback=True),
|
|
||||||
'stop': config.getboolean('perms', 'stop', fallback=False),
|
|
||||||
'status': config.getboolean('perms', 'status', fallback=True),
|
|
||||||
'dev_commands': config.getboolean('perms', 'false', fallback=False),
|
|
||||||
}
|
|
||||||
|
|
||||||
config.read(f'{os.path.join(*path_list, "") or ""}settings.ini')
|
|
||||||
|
|
||||||
self.log_path = config.get('logging', 'path', fallback=os.path.join('logs', ''))
|
|
||||||
self.log_level = config.getint('logging', 'level', fallback=logging.INFO)
|
|
||||||
|
|
||||||
self.retry_in_seconds = config.getint('presence', 'retry_in_seconds', fallback=15)
|
|
||||||
self.server_start_timout = config.getint('presence', 'server_start_timout', fallback=300)
|
|
||||||
|
|
||||||
self.debug_guilds = config.get('debug', 'debug_guilds', fallback=None)
|
|
||||||
if self.debug_guilds:
|
|
||||||
self.debug_guilds = [int(guild_id) for guild_id in self.debug_guilds.split(' ')]
|
|
||||||
|
|
||||||
self.intents = discord.Intents.none()
|
|
||||||
self.mc_flags = discord.MemberCacheFlags.from_intents(self.intents)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def auth_token(self) -> str:
|
|
||||||
"""< property >
|
|
||||||
|
|
||||||
This ensures the authentication token can only be used once.
|
|
||||||
After that it gets deleted for protection reasons
|
|
||||||
|
|
||||||
:return: OAuth Token
|
|
||||||
"""
|
|
||||||
|
|
||||||
try:
|
|
||||||
token = self.__login_token
|
|
||||||
del self.__login_token
|
|
||||||
|
|
||||||
except AttributeError:
|
|
||||||
raise Exception('OAuth token has already been used once')
|
|
||||||
|
|
||||||
else:
|
|
||||||
return token
|
|
||||||
|
|
||||||
|
|
||||||
class BotClient(discord.Bot, ABC):
|
|
||||||
"""< discord.Bot >
|
|
||||||
|
|
||||||
The bot class
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.__config = Configs()
|
|
||||||
self.logger = self.__setup_logger(self.__config.log_level)
|
|
||||||
|
|
||||||
super(BotClient, self).__init__(
|
|
||||||
# default presence
|
|
||||||
activity=discord.Game('Beep Boop! Loading...'),
|
|
||||||
status=discord.Status.idle,
|
|
||||||
|
|
||||||
# debug
|
|
||||||
debug_guilds=self.config.debug_guilds,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.is_server_starting = False
|
|
||||||
self.last_start = time.time()
|
|
||||||
|
|
||||||
self.mc_server = JavaServer(
|
|
||||||
self.__config.server_address,
|
|
||||||
self.__config.server_port
|
|
||||||
)
|
|
||||||
self.presence_manager = Presence(self)
|
|
||||||
|
|
||||||
# extensions
|
|
||||||
self.add_cog(Status(self))
|
|
||||||
self.add_cog(Developer(self))
|
|
||||||
|
|
||||||
def run(self, *args: Any, **kwargs: Any) -> None:
|
|
||||||
"""< function >
|
|
||||||
|
|
||||||
Starts the bot and automatically gets the configured token.
|
|
||||||
"""
|
|
||||||
|
|
||||||
super(BotClient, self).run(self.__config.auth_token)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def config(self) -> Configs:
|
|
||||||
"""< property >
|
|
||||||
|
|
||||||
The default config should not be changeable even on runtime.
|
|
||||||
This ensures its read-only.
|
|
||||||
|
|
||||||
:return: Butter default config
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.__config
|
|
||||||
|
|
||||||
@property
|
|
||||||
def color(self) -> int:
|
|
||||||
"""< property >
|
|
||||||
|
|
||||||
Depending on the setting set in ButterConfig either
|
|
||||||
the bot's default color gets returned or a random
|
|
||||||
always bright color.
|
|
||||||
|
|
||||||
:return: hex-Color
|
|
||||||
"""
|
|
||||||
|
|
||||||
colors: list[int] = [0, 255, random.randint(0, 255)]
|
|
||||||
random.shuffle(colors)
|
|
||||||
|
|
||||||
return int('0x%02x%02x%02x' % tuple(colors), 16)
|
|
||||||
|
|
||||||
def __setup_logger(self, level: int = logging.INFO) -> logging.Logger:
|
|
||||||
"""< function >
|
|
||||||
|
|
||||||
Basic logging abilities
|
|
||||||
"""
|
|
||||||
|
|
||||||
path_list = re.split('/| \\\\', self.__config.log_path)
|
|
||||||
|
|
||||||
for i, folder in enumerate(path_list):
|
|
||||||
# filtering empty strings (e.g. for using source folder)
|
|
||||||
if not folder:
|
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
|
||||||
# creates folders
|
|
||||||
os.mkdir(os.path.join(*path_list[:i + 1]))
|
|
||||||
|
|
||||||
except FileExistsError:
|
|
||||||
continue
|
|
||||||
|
|
||||||
logger = logging.getLogger('discord')
|
|
||||||
logger.setLevel(level)
|
|
||||||
|
|
||||||
formatter = logging.Formatter(fmt='[%(asctime)s] - %(levelname)s: %(name)s: %(message)s')
|
|
||||||
|
|
||||||
file_handler = logging.FileHandler(filename=f'{os.path.join(*path_list, "") or ""}discord.log',
|
|
||||||
encoding='utf-8', mode='w')
|
|
||||||
file_handler.setFormatter(formatter)
|
|
||||||
logger.addHandler(file_handler)
|
|
||||||
|
|
||||||
console_handler = logging.StreamHandler()
|
|
||||||
console_handler.setFormatter(formatter)
|
|
||||||
logger.addHandler(console_handler)
|
|
||||||
|
|
||||||
return logger
|
|
||||||
|
|
||||||
async def on_ready(self) -> None:
|
|
||||||
"""< coroutine >
|
|
||||||
|
|
||||||
Logs when the bot is online.
|
|
||||||
"""
|
|
||||||
|
|
||||||
self.logger.info('Bot successfully started')
|
|
||||||
|
|
||||||
|
|
||||||
class Presence(object):
|
|
||||||
"""<object>
|
|
||||||
|
|
||||||
A class for simply managing the bot's presence.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, bot: BotClient):
|
|
||||||
self.bot = bot
|
|
||||||
self.mc_server = self.bot.mc_server
|
|
||||||
self.retry_in_seconds = self.bot.config.retry_in_seconds
|
|
||||||
|
|
||||||
self.__server_online_presence.start()
|
|
||||||
|
|
||||||
@tasks.loop()
|
|
||||||
async def __server_online_presence(self):
|
|
||||||
_time = time.time()
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.bot.logger.debug('Getting server information')
|
|
||||||
status = await self.mc_server.async_status()
|
|
||||||
|
|
||||||
self.bot.is_server_starting = False
|
|
||||||
|
|
||||||
await self.bot.change_presence(
|
|
||||||
activity=discord.Game(
|
|
||||||
name=f'with {status.players.online} player{"s" if status.players.online != 1 else ""}',
|
|
||||||
),
|
|
||||||
status=discord.Status.online
|
|
||||||
)
|
|
||||||
|
|
||||||
await asyncio.sleep(40)
|
|
||||||
await self.bot.change_presence(
|
|
||||||
activity=discord.Activity(
|
|
||||||
type=discord.ActivityType.watching,
|
|
||||||
name=self.bot.config.server_address,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
await asyncio.sleep(20)
|
|
||||||
|
|
||||||
except asyncio.TimeoutError:
|
|
||||||
if self.bot.is_server_starting and self.bot.last_start + self.bot.config.server_start_timout < time.time():
|
|
||||||
self.bot.is_server_starting = False
|
|
||||||
|
|
||||||
# ToDo: better presence
|
|
||||||
await self.bot.change_presence(
|
|
||||||
activity=discord.Activity(
|
|
||||||
type=discord.ActivityType.watching,
|
|
||||||
name=f'{self.bot.config.server_address} starting' if self.bot.is_server_starting else
|
|
||||||
f'offline: [self.server.address]',
|
|
||||||
),
|
|
||||||
status=discord.Status.idle
|
|
||||||
)
|
|
||||||
|
|
||||||
# abs -> simple fix if for any reason the sleep_time is negative
|
|
||||||
sleep_time = abs(_time - time.time() + self.retry_in_seconds)
|
|
||||||
self.bot.logger.debug(f'Server offline, retrying in {round(sleep_time, 4)} seconds')
|
|
||||||
await asyncio.sleep(sleep_time)
|
|
||||||
|
|
||||||
@__server_online_presence.before_loop
|
|
||||||
async def __before_status(self) -> None:
|
|
||||||
"""< coroutine >
|
|
||||||
|
|
||||||
Waits until the bot has fully started in case the server
|
|
||||||
is already online
|
|
||||||
"""
|
|
||||||
|
|
||||||
await self.bot.wait_until_ready()
|
|
||||||
self.bot.logger.info('presence loaded')
|
|
||||||
|
|
||||||
|
|
||||||
class Developer(discord.Cog):
|
|
||||||
"""< discord.Cog >
|
|
||||||
|
|
||||||
Some developer tools for managing the bot.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, bot: BotClient):
|
|
||||||
self.bot = bot
|
|
||||||
|
|
||||||
async def __permission_granter(self, *path: str) -> int:
|
|
||||||
process = await asyncio.create_subprocess_shell(
|
|
||||||
cmd=f'sudo chmod +x {os.path.join(os.getcwd(), *path)}',
|
|
||||||
stdout=asyncio.subprocess.PIPE,
|
|
||||||
stderr=asyncio.subprocess.PIPE,
|
|
||||||
)
|
|
||||||
stdout, stderr = await process.communicate()
|
|
||||||
|
|
||||||
self.bot.logger.info(f'Finished bot update with exit code {process.returncode}')
|
|
||||||
if process.returncode:
|
|
||||||
self.bot.logger.error(f'stderr:\n{stderr.decode()}')
|
|
||||||
|
|
||||||
else:
|
|
||||||
self.bot.logger.info(f'stdout:\n{stdout.decode()}')
|
|
||||||
|
|
||||||
return process.returncode
|
|
||||||
|
|
||||||
__dev_group = SlashCommandGroup(name='dev', description='Developer settings')
|
|
||||||
|
|
||||||
@__dev_group.command(name='update')
|
|
||||||
async def __update_bot(self, ctx: discord.ApplicationContext):
|
|
||||||
response = await ctx.respond('starting update...')
|
|
||||||
|
|
||||||
if await self.__permission_granter('scripts', 'update.sh') == 0:
|
|
||||||
print('okay')
|
|
||||||
|
|
||||||
else:
|
|
||||||
await ctx.respond('Failed to update. Check the log files for further information')
|
|
||||||
|
|
||||||
|
|
||||||
class Status(discord.Cog):
|
|
||||||
"""< discord.Cog >
|
|
||||||
|
|
||||||
An extension to display servers status
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, bot: BotClient):
|
|
||||||
self.bot = bot
|
|
||||||
self.mc_server = self.bot.mc_server
|
|
||||||
|
|
||||||
@slash_command(name='info')
|
|
||||||
async def __show_server_info(self, ctx: discord.ApplicationContext) -> None:
|
|
||||||
print()
|
|
||||||
"""
|
|
||||||
.add_field(
|
|
||||||
name='Version',
|
|
||||||
value="\n".join(re.split(", | ", status.version.name)),
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
|
|
||||||
@slash_command(name='status')
|
|
||||||
async def __show_status(self, ctx: discord.ApplicationContext) -> None:
|
|
||||||
print('exec')
|
|
||||||
await ctx.defer()
|
|
||||||
try:
|
|
||||||
status: mcstatus.pinger.PingResponse = await self.mc_server.async_status()
|
|
||||||
pprint.pprint(vars(status))
|
|
||||||
|
|
||||||
query = await self.mc_server.async_query()
|
|
||||||
pprint.pprint(vars(query))
|
|
||||||
|
|
||||||
await ctx.respond(
|
|
||||||
embed=discord.Embed(
|
|
||||||
title=f'Minecraft server status!',
|
|
||||||
description=f'(**{self.bot.config.server_address}** on port {self.bot.config.server_port})',
|
|
||||||
colour=self.bot.color,
|
|
||||||
timestamp=datetime.now()
|
|
||||||
).add_field(
|
|
||||||
name='Server Ping',
|
|
||||||
value=f'**﹂**``{round(status.latency, 2)} ms``',
|
|
||||||
# value=f'**⌊** **🏓 Pong!** with **``{round(status.latency, 2)}``** ms'
|
|
||||||
).add_field(
|
|
||||||
name='Players online',
|
|
||||||
value=f'**﹂** ``{status.players.online} players``',
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
except asyncio.TimeoutError or OSError:
|
|
||||||
# ToDo: Check os Error on booting
|
|
||||||
# ToDo: add is starting and create embed
|
|
||||||
await ctx.respond(embed=discord.Embed(
|
|
||||||
title=f'Server offline',
|
|
||||||
description=f'(**{self.bot.config.server_address}** on port {self.bot.config.server_port})',
|
|
||||||
colour=self.bot.color,
|
|
||||||
timestamp=datetime.now()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class StartStop(discord.Cog):
|
|
||||||
"""< discord.Cog >
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
BotClient().run()
|
|
@ -1,3 +1,7 @@
|
|||||||
|
# Project is under GNU GENERAL PUBLIC LICENSE 3.0
|
||||||
|
#
|
||||||
|
# 2022, created by Specoolazius
|
||||||
|
|
||||||
# These are the settings you need to configure.
|
# These are the settings you need to configure.
|
||||||
# Rename this file to "config.ini"
|
# Rename this file to "config.ini"
|
||||||
# remove ; to modify further options
|
# remove ; to modify further options
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
|
# Project is under GNU GENERAL PUBLIC LICENSE 3.0
|
||||||
|
#
|
||||||
|
# 2022, created by Specoolazius
|
||||||
|
|
||||||
# You can change setting in here if you wish but it works
|
# You can change setting in here if you wish but it works
|
||||||
# fine as it is configured by default
|
# fine as it is configured by default
|
||||||
# remove ; to modify options
|
# remove ; to modify options
|
||||||
@ -19,3 +23,6 @@
|
|||||||
# create the slash commands globally
|
# create the slash commands globally
|
||||||
# enter multiple guild ids by separating them with space
|
# enter multiple guild ids by separating them with space
|
||||||
debug_guilds = 848137923101982741 958692739065720832 418447236008116228
|
debug_guilds = 848137923101982741 958692739065720832 418447236008116228
|
||||||
|
|
||||||
|
[systemd]
|
||||||
|
; service_name = mc-status-bot
|
@ -0,0 +1,9 @@
|
|||||||
|
"""
|
||||||
|
Project is under GNU GENERAL PUBLIC LICENSE 3.0
|
||||||
|
|
||||||
|
2022, created by Specoolazius
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .admin import Admin
|
||||||
|
from .startstop import StartStop
|
||||||
|
from .status import Status
|
43
bot/extensions/admin.py
Normal file
43
bot/extensions/admin.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
"""
|
||||||
|
Project is under GNU GENERAL PUBLIC LICENSE 3.0
|
||||||
|
|
||||||
|
2022, created by Specoolazius
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from discord.commands import SlashCommandGroup
|
||||||
|
|
||||||
|
from libs import Client
|
||||||
|
|
||||||
|
|
||||||
|
class Admin(discord.Cog):
|
||||||
|
"""< discord.Cog >
|
||||||
|
|
||||||
|
Some developer tools for managing the bot.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, bot: Client):
|
||||||
|
self.bot = bot
|
||||||
|
|
||||||
|
__dev_group = SlashCommandGroup(name='dev', description='Developer settings')
|
||||||
|
|
||||||
|
@__dev_group.command(name='update')
|
||||||
|
async def __update_bot(self, ctx: discord.ApplicationContext) -> None:
|
||||||
|
await ctx.respond('starting update...')
|
||||||
|
|
||||||
|
if 0 == await self.bot.execute_shell('update.sh'):
|
||||||
|
await ctx.respond('Updated bot from https://github.com/Specoolazius/mcserver-discordbot\n'
|
||||||
|
'You may need to restart the bot')
|
||||||
|
|
||||||
|
else:
|
||||||
|
await ctx.respond(f'Failed to update bot. Check {os.path.join(self.bot.config.log_path, "discord.log")} '
|
||||||
|
f'for more detailed information')
|
||||||
|
|
||||||
|
@__dev_group.command(name='restart')
|
||||||
|
async def __restart_service(self, ctx: discord.ApplicationContext) -> None:
|
||||||
|
await ctx.respond('attempting restart...')
|
||||||
|
await asyncio.create_subprocess_shell(f'sudo systemctl restart')
|
||||||
|
|
24
bot/extensions/startstop.py
Normal file
24
bot/extensions/startstop.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
"""
|
||||||
|
Project is under GNU GENERAL PUBLIC LICENSE 3.0
|
||||||
|
|
||||||
|
2022, created by Specoolazius
|
||||||
|
"""
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from discord.commands import slash_command
|
||||||
|
|
||||||
|
from libs import Client
|
||||||
|
|
||||||
|
|
||||||
|
class StartStop(discord.Cog):
|
||||||
|
"""< discord.Cog >
|
||||||
|
|
||||||
|
Extension for starting and stopping the Minecraft server.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, bot: Client):
|
||||||
|
self.bot = bot
|
||||||
|
|
||||||
|
@slash_command(name='start')
|
||||||
|
async def __execute_start(self, ctx: discord.ApplicationContext) -> None:
|
||||||
|
pass
|
75
bot/extensions/status.py
Normal file
75
bot/extensions/status.py
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
"""
|
||||||
|
Project is under GNU GENERAL PUBLIC LICENSE 3.0
|
||||||
|
|
||||||
|
2022, created by Specoolazius
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from datetime import datetime
|
||||||
|
import pprint
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from discord.commands import slash_command
|
||||||
|
import mcstatus.pinger
|
||||||
|
|
||||||
|
from libs import Client
|
||||||
|
|
||||||
|
|
||||||
|
class Status(discord.Cog):
|
||||||
|
"""< discord.Cog >
|
||||||
|
|
||||||
|
An extension to display servers status
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, bot: Client):
|
||||||
|
self.bot = bot
|
||||||
|
self.mc_server = self.bot.mc_server
|
||||||
|
|
||||||
|
@slash_command(name='info')
|
||||||
|
async def __show_server_info(self, ctx: discord.ApplicationContext) -> None:
|
||||||
|
print()
|
||||||
|
"""
|
||||||
|
.add_field(
|
||||||
|
name='Version',
|
||||||
|
value="\n".join(re.split(", | ", status.version.name)),
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
|
||||||
|
@slash_command(name='status')
|
||||||
|
async def __show_status(self, ctx: discord.ApplicationContext) -> None:
|
||||||
|
print('exec')
|
||||||
|
await ctx.defer()
|
||||||
|
try:
|
||||||
|
status: mcstatus.pinger.PingResponse = await self.mc_server.async_status()
|
||||||
|
pprint.pprint(vars(status))
|
||||||
|
|
||||||
|
query = await self.mc_server.async_query()
|
||||||
|
pprint.pprint(vars(query))
|
||||||
|
|
||||||
|
await ctx.respond(
|
||||||
|
embed=discord.Embed(
|
||||||
|
title=f'Minecraft server status!',
|
||||||
|
description=f'(**{self.bot.config.server_address}** on port {self.bot.config.server_port})',
|
||||||
|
colour=self.bot.color,
|
||||||
|
timestamp=datetime.now()
|
||||||
|
).add_field(
|
||||||
|
name='Server Ping',
|
||||||
|
value=f'**﹂**``{round(status.latency, 2)} ms``',
|
||||||
|
# value=f'**⌊** **🏓 Pong!** with **``{round(status.latency, 2)}``** ms'
|
||||||
|
).add_field(
|
||||||
|
name='Players online',
|
||||||
|
value=f'**﹂** ``{status.players.online} players``',
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
except asyncio.TimeoutError or OSError:
|
||||||
|
# ToDo: Check os Error on booting
|
||||||
|
# ToDo: add is starting and create embed
|
||||||
|
await ctx.respond(
|
||||||
|
embed=discord.Embed(
|
||||||
|
title=f'Server offline',
|
||||||
|
description=f'(**{self.bot.config.server_address}** on port {self.bot.config.server_port})',
|
||||||
|
colour=self.bot.color,
|
||||||
|
timestamp=datetime.now()
|
||||||
|
)
|
||||||
|
)
|
9
bot/libs/__init__.py
Normal file
9
bot/libs/__init__.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
"""
|
||||||
|
Project is under GNU GENERAL PUBLIC LICENSE 3.0
|
||||||
|
|
||||||
|
2022, created by Specoolazius
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .config import Configs
|
||||||
|
from .presence import Presence
|
||||||
|
from .client import Client
|
180
bot/libs/client.py
Normal file
180
bot/libs/client.py
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
"""
|
||||||
|
Project is under GNU GENERAL PUBLIC LICENSE 3.0
|
||||||
|
|
||||||
|
2022, created by Specoolazius
|
||||||
|
"""
|
||||||
|
|
||||||
|
from abc import ABC
|
||||||
|
import asyncio
|
||||||
|
from typing import Any
|
||||||
|
import re
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from mcstatus import JavaServer
|
||||||
|
|
||||||
|
from libs import Configs, Presence
|
||||||
|
|
||||||
|
SHELL_SCRIPT_PATH = 'scripts'
|
||||||
|
|
||||||
|
|
||||||
|
class Client(discord.Bot, ABC):
|
||||||
|
"""< discord.Bot >
|
||||||
|
|
||||||
|
The bot class
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.__config = Configs()
|
||||||
|
self.logger = self.__setup_logger(self.__config.log_level)
|
||||||
|
|
||||||
|
super(Client, self).__init__(
|
||||||
|
# default presence
|
||||||
|
activity=discord.Game('Beep Boop! Loading...'),
|
||||||
|
status=discord.Status.idle,
|
||||||
|
|
||||||
|
# debug
|
||||||
|
debug_guilds=self.config.debug_guilds,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.is_server_starting = False
|
||||||
|
self.last_start = time.time()
|
||||||
|
|
||||||
|
self.mc_server = JavaServer(
|
||||||
|
self.__config.server_address,
|
||||||
|
self.__config.server_port
|
||||||
|
)
|
||||||
|
self.presence_manager = Presence(self)
|
||||||
|
|
||||||
|
# extensions
|
||||||
|
from extensions import Admin, StartStop, Status
|
||||||
|
|
||||||
|
for module in [Admin, StartStop, Status]:
|
||||||
|
self.add_cog(module(self))
|
||||||
|
|
||||||
|
def run(self, *args: Any, **kwargs: Any) -> None:
|
||||||
|
"""< function >
|
||||||
|
|
||||||
|
Starts the bot and automatically gets the configured token.
|
||||||
|
"""
|
||||||
|
|
||||||
|
super(Client, self).run(self.__config.auth_token)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def config(self) -> Configs:
|
||||||
|
"""< property >
|
||||||
|
|
||||||
|
The default config should not be changeable even on runtime.
|
||||||
|
This ensures its read-only.
|
||||||
|
|
||||||
|
:return: Butter default config
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.__config
|
||||||
|
|
||||||
|
@property
|
||||||
|
def color(self) -> int:
|
||||||
|
"""< property >
|
||||||
|
|
||||||
|
Depending on the setting set in ButterConfig either
|
||||||
|
the bot's default color gets returned or a random
|
||||||
|
always bright color.
|
||||||
|
|
||||||
|
:return: hex-Color
|
||||||
|
"""
|
||||||
|
|
||||||
|
colors: list[int] = [0, 255, random.randint(0, 255)]
|
||||||
|
random.shuffle(colors)
|
||||||
|
|
||||||
|
return int('0x%02x%02x%02x' % tuple(colors), 16)
|
||||||
|
|
||||||
|
def __setup_logger(self, level: int = logging.INFO) -> logging.Logger:
|
||||||
|
"""< function >
|
||||||
|
|
||||||
|
Basic logging abilities
|
||||||
|
"""
|
||||||
|
|
||||||
|
path_list = re.split('/| \\\\', self.__config.log_path)
|
||||||
|
|
||||||
|
for i, folder in enumerate(path_list):
|
||||||
|
# filtering empty strings (e.g. for using source folder)
|
||||||
|
if not folder:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
# creates folders
|
||||||
|
os.mkdir(os.path.join(*path_list[:i + 1]))
|
||||||
|
|
||||||
|
except FileExistsError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
logger = logging.getLogger('discord')
|
||||||
|
logger.setLevel(level)
|
||||||
|
|
||||||
|
formatter = logging.Formatter(fmt='[%(asctime)s] - %(levelname)s: %(name)s: %(message)s')
|
||||||
|
|
||||||
|
file_handler = logging.FileHandler(filename=f'{os.path.join(*path_list, "") or ""}discord.log',
|
||||||
|
encoding='utf-8', mode='w')
|
||||||
|
file_handler.setFormatter(formatter)
|
||||||
|
logger.addHandler(file_handler)
|
||||||
|
|
||||||
|
console_handler = logging.StreamHandler()
|
||||||
|
console_handler.setFormatter(formatter)
|
||||||
|
logger.addHandler(console_handler)
|
||||||
|
|
||||||
|
return logger
|
||||||
|
|
||||||
|
async def on_ready(self) -> None:
|
||||||
|
"""< coroutine >
|
||||||
|
|
||||||
|
Logs when the bot is online.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.logger.info('Bot successfully started')
|
||||||
|
|
||||||
|
async def execute_shell(self, file_name: str, retry=True) -> int:
|
||||||
|
"""< coroutine >
|
||||||
|
|
||||||
|
Runs a bash script in executer and returns the process code.
|
||||||
|
Logs errors if process returncode isn't 0.
|
||||||
|
|
||||||
|
:param file_name: file name
|
||||||
|
:param retry: recursion stopper in case granting permissions fails.
|
||||||
|
:return: process returncode
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def __grant_permission() -> int:
|
||||||
|
self.logger.info(f'Granting permissions to {file_name}...')
|
||||||
|
|
||||||
|
process_chmod = await asyncio.create_subprocess_shell(
|
||||||
|
cmd=f'chmod +x {os.path.join(os.getcwd(), SHELL_SCRIPT_PATH, file_name)}'
|
||||||
|
)
|
||||||
|
|
||||||
|
await process_chmod.communicate()
|
||||||
|
return process_chmod.returncode
|
||||||
|
|
||||||
|
process = await asyncio.create_subprocess_exec(
|
||||||
|
program=os.path.join(os.getcwd(), SHELL_SCRIPT_PATH, file_name),
|
||||||
|
stdout=asyncio.subprocess.PIPE,
|
||||||
|
stderr=asyncio.subprocess.PIPE,
|
||||||
|
)
|
||||||
|
|
||||||
|
stdout, stderr = await process.communicate()
|
||||||
|
self.logger.info(f'Executed script {file_name} with exit code {process.returncode}')
|
||||||
|
|
||||||
|
if process.returncode == 0:
|
||||||
|
self.logger.info(f'stdout:\n{stdout.decode()}')
|
||||||
|
|
||||||
|
# bash returncode 126: permission error
|
||||||
|
elif process.returncode == 126 and retry:
|
||||||
|
# retrying once
|
||||||
|
self.logger.warning(f'Missing permissions for {file_name}')
|
||||||
|
return await self.execute_shell(file_name, retry=False)
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.logger.error(f'stderr:\n{stderr.decode()}')
|
||||||
|
|
||||||
|
return process.returncode
|
86
bot/libs/config.py
Normal file
86
bot/libs/config.py
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
"""
|
||||||
|
Project is under GNU GENERAL PUBLIC LICENSE 3.0
|
||||||
|
|
||||||
|
2022, created by Specoolazius
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from configparser import ConfigParser
|
||||||
|
import re
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import discord
|
||||||
|
|
||||||
|
CONFIG_PATH = 'config'
|
||||||
|
|
||||||
|
|
||||||
|
class Configs(object):
|
||||||
|
"""< object >
|
||||||
|
|
||||||
|
This contains all configs and settings defined in config.ini and settings.ini
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.start_time = datetime.now()
|
||||||
|
path_list = re.split('/| \\\\', CONFIG_PATH)
|
||||||
|
|
||||||
|
config = ConfigParser(allow_no_value=True)
|
||||||
|
config.read(f'{os.path.join(*path_list, "") or ""}config.ini')
|
||||||
|
|
||||||
|
self.__login_token = config.get('discord', 'token')
|
||||||
|
|
||||||
|
self.server_address = config.get('mcserver', 'server_address')
|
||||||
|
self.server_port = config.getint('mcserver', 'server_port', fallback=25565)
|
||||||
|
|
||||||
|
self.admin_ids = config.get('perms', 'admin_ids')
|
||||||
|
if self.admin_ids:
|
||||||
|
self.admin_ids = [int(admin_id) for admin_id in self.admin_ids.split(' ')]
|
||||||
|
|
||||||
|
self.role_ids = config.get('perms', 'role_ids')
|
||||||
|
if self.admin_ids:
|
||||||
|
self.role_ids = [int(admin_id) for admin_id in self.admin_ids.split(' ')]
|
||||||
|
|
||||||
|
self.executable_commands = {
|
||||||
|
'start': config.getboolean('perms', 'start', fallback=True),
|
||||||
|
'stop': config.getboolean('perms', 'stop', fallback=False),
|
||||||
|
'status': config.getboolean('perms', 'status', fallback=True),
|
||||||
|
'dev_commands': config.getboolean('perms', 'false', fallback=False),
|
||||||
|
}
|
||||||
|
|
||||||
|
config.read(f'{os.path.join(*path_list, "") or ""}settings.ini')
|
||||||
|
|
||||||
|
self.log_path = config.get('logging', 'path', fallback=os.path.join('logs', ''))
|
||||||
|
self.log_level = config.getint('logging', 'level', fallback=logging.INFO)
|
||||||
|
|
||||||
|
self.retry_in_seconds = config.getint('presence', 'retry_in_seconds', fallback=15)
|
||||||
|
self.server_start_timout = config.getint('presence', 'server_start_timout', fallback=300)
|
||||||
|
|
||||||
|
self.debug_guilds = config.get('debug', 'debug_guilds', fallback=None)
|
||||||
|
if self.debug_guilds:
|
||||||
|
self.debug_guilds = [int(guild_id) for guild_id in self.debug_guilds.split(' ')]
|
||||||
|
|
||||||
|
self.service_name = config.get('systemd', 'service_name', fallback='mc-status-bot')
|
||||||
|
|
||||||
|
self.intents = discord.Intents.none()
|
||||||
|
self.mc_flags = discord.MemberCacheFlags.from_intents(self.intents)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def auth_token(self) -> str:
|
||||||
|
"""< property >
|
||||||
|
|
||||||
|
This ensures the authentication token can only be used once.
|
||||||
|
After that it gets deleted for protection reasons
|
||||||
|
|
||||||
|
:return: OAuth Token
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
token = self.__login_token
|
||||||
|
del self.__login_token
|
||||||
|
|
||||||
|
except AttributeError:
|
||||||
|
raise Exception('OAuth token has already been used once')
|
||||||
|
|
||||||
|
else:
|
||||||
|
return token
|
85
bot/libs/presence.py
Normal file
85
bot/libs/presence.py
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
"""
|
||||||
|
Project is under GNU GENERAL PUBLIC LICENSE 3.0
|
||||||
|
|
||||||
|
2022, created by Specoolazius
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import time
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from discord.ext import tasks
|
||||||
|
|
||||||
|
|
||||||
|
class Presence(object):
|
||||||
|
"""<object>
|
||||||
|
|
||||||
|
A class for simply managing the bot's presence.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, bot):
|
||||||
|
from libs import Client
|
||||||
|
bot: Client
|
||||||
|
|
||||||
|
self.bot = bot
|
||||||
|
self.mc_server = self.bot.mc_server
|
||||||
|
self.retry_in_seconds = self.bot.config.retry_in_seconds
|
||||||
|
|
||||||
|
self.__server_online_presence.start()
|
||||||
|
|
||||||
|
@tasks.loop()
|
||||||
|
async def __server_online_presence(self):
|
||||||
|
_time = time.time()
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.bot.logger.debug('Getting server information')
|
||||||
|
status = await self.mc_server.async_status()
|
||||||
|
|
||||||
|
self.bot.is_server_starting = False
|
||||||
|
|
||||||
|
await self.bot.change_presence(
|
||||||
|
activity=discord.Game(
|
||||||
|
name=f'with {status.players.online} player{"s" if status.players.online != 1 else ""}',
|
||||||
|
),
|
||||||
|
status=discord.Status.online
|
||||||
|
)
|
||||||
|
|
||||||
|
await asyncio.sleep(40)
|
||||||
|
await self.bot.change_presence(
|
||||||
|
activity=discord.Activity(
|
||||||
|
type=discord.ActivityType.watching,
|
||||||
|
name=self.bot.config.server_address,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
await asyncio.sleep(20)
|
||||||
|
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
if self.bot.is_server_starting and self.bot.last_start + self.bot.config.server_start_timout < time.time():
|
||||||
|
self.bot.is_server_starting = False
|
||||||
|
|
||||||
|
# ToDo: better presence
|
||||||
|
await self.bot.change_presence(
|
||||||
|
activity=discord.Activity(
|
||||||
|
type=discord.ActivityType.watching,
|
||||||
|
name=f'{self.bot.config.server_address} starting' if self.bot.is_server_starting else
|
||||||
|
f'offline: [self.server.address]',
|
||||||
|
),
|
||||||
|
status=discord.Status.idle
|
||||||
|
)
|
||||||
|
|
||||||
|
# abs -> simple fix if for any reason the sleep_time is negative
|
||||||
|
sleep_time = abs(_time - time.time() + self.retry_in_seconds)
|
||||||
|
self.bot.logger.debug(f'Server offline, retrying in {round(sleep_time, 4)} seconds')
|
||||||
|
await asyncio.sleep(sleep_time)
|
||||||
|
|
||||||
|
@__server_online_presence.before_loop
|
||||||
|
async def __before_status(self) -> None:
|
||||||
|
"""< coroutine >
|
||||||
|
|
||||||
|
Waits until the bot has fully started in case the server
|
||||||
|
is already online
|
||||||
|
"""
|
||||||
|
|
||||||
|
await self.bot.wait_until_ready()
|
||||||
|
self.bot.logger.info('presence loaded')
|
12
bot/run.py
12
bot/run.py
@ -1,14 +1,18 @@
|
|||||||
|
"""
|
||||||
|
Project is under GNU GENERAL PUBLIC LICENSE 3.0
|
||||||
|
|
||||||
|
2022, created by Specoolazius
|
||||||
|
"""
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
from aiohttp import ClientConnectorError
|
from aiohttp import ClientConnectorError
|
||||||
|
|
||||||
from libs.bot import ServerBot
|
from libs import Client
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
bot = ServerBot()
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
bot.run()
|
Client().run()
|
||||||
|
|
||||||
except ClientConnectorError as e:
|
except ClientConnectorError as e:
|
||||||
sys.exit(-59)
|
sys.exit(-59)
|
||||||
|
8
bot/run.sh
Normal file
8
bot/run.sh
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Project is under GNU GENERAL PUBLIC LICENSE 3.0
|
||||||
|
#
|
||||||
|
# 2022, created by Specoolazius
|
||||||
|
|
||||||
|
cd ~/mcserver-discordbot/bot || exit;
|
||||||
|
sudo python3.10 -O run.py
|
@ -1 +1,10 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Project is under GNU GENERAL PUBLIC LICENSE 3.0
|
||||||
|
#
|
||||||
|
# 2022, created by Specoolazius
|
||||||
|
|
||||||
|
# Updates bot from https://github.com/Specoolazius/mcserver-discordbot
|
||||||
|
|
||||||
|
git stash
|
||||||
|
git pull --rebase origin
|
||||||
|
12
mc-status-bot.service
Normal file
12
mc-status-bot.service
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# paste this file to /usr/lib/systemd/system/mc-status-bot.service
|
||||||
|
|
||||||
|
[Unit]
|
||||||
|
Description=Minecraft Status Bot
|
||||||
|
Requires=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=idle
|
||||||
|
ExecStart=~/mcserver-discordbot/bot/run.sh
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
Loading…
Reference in New Issue
Block a user