import discord from discord.ext import commands import json from srcds import rcon as rcon_con import time, logging from datetime import datetime import asyncio import traceback from .imports import checks config_dir = 'config' admin_id_file = 'admin_ids' extension_dir = 'extensions' owner_id = 351794468870946827 guild_config_file = 'guild_config' rcon_config_file = 'server_rcon_config' guild_config_dir = 'guild_config/' rcon_log = logging.getLogger('rcon') game_commands = ['admin',] game_prefix = '$' class rcon(): def __init__(self, bot): self.bot = bot def _listplayers(self, con_info): con = rcon_con.RconConnection(con_info['ip'], con_info['port'], con_info['password']) asyncio.sleep(5) response = con.exec_command('listplayers') rcon_log.info(response) while response == b'Keep Alive\x00\x00': asyncio.sleep(5) response = con.exec_command('listplayers') rcon_log.info(response) return response.strip(b'\n \x00\x00').decode('ascii').strip() def _saveworld(self, con_info): con = rcon_con.RconConnection(con_info['ip'], con_info['port'], con_info['password']) asyncio.sleep(5) response = con.exec_command('saveworld') rcon_log.info(response) while response == b'Keep Alive\x00\x00': asyncio.sleep(5) response = con.exec_command('saveworld') rcon_log.info(response) return response.strip(b'\n \x00\x00').decode('ascii').strip() def _whitelist(self, con_info, steam_ids): messages = [] con = rcon_con.RconConnection(con_info['ip'], con_info['port'], con_info['password']) asyncio.sleep(5) for steam_id in steam_ids: response = con.exec_command('AllowPlayerToJoinNoCheck {0}'.format(steam_id)) rcon_log.info(response) while response == b'Keep Alive\x00\x00': asyncio.sleep(5) response = con.exec_command('AllowPlayerToJoinNoCheck {0}'.format(steam_id)) rcon_log.info(response) messages.append(response.strip(b'\n \x00\x00').decode('ascii').strip()) return messages def _broadcast(self, con_info, message): messages = [] con = rcon_con.RconConnection(con_info['ip'], con_info['port'], con_info['password']) asyncio.sleep(5) response = con.exec_command('broadcast {0}'.format(message)) rcon_log.info(response) while response == b'Keep Alive\x00\x00': asyncio.sleep(5) response = con.exec_command('broadcast {0}'.format(message)) rcon_log.info(response) messages.append(response.strip(b'\n \x00\x00').decode('ascii').strip()) return messages def _get_current_chat(self, con): response = con.exec_command('getchat') rcon_log.debug(response) return response.strip(b'\n \x00\x00').decode('ascii').strip() def server_chat_background_process(self, guild_id, con): return_messages = [] try: message = 'Server received, But no response!!' time_now = datetime.now().timestamp() while 'Server received, But no response!!' in message and time_now + 20 > datetime.now().timestamp(): message = self._get_current_chat(con) rcon_log.debug(message) time.sleep(1) if 'Server received, But no response!!' not in message: for msg in message.split('\n'): msg = '{0} ||| {1}'.format(datetime.now().strftime('%Y-%m-%d %H:%M:%S'),msg.strip()) return_messages.append(msg) except rcon_con.RconError as e: rcon_log.error('RCON Error {0}\n{1}'.format(guild_id,traceback.format_exc())) return_messages.append('RCON Error') except Exception as e: rcon_log.error('Exception {0}\n{1}'.format(guild_id,traceback.format_exc())) return_messages.append('Exception') return_messages.append(e) rcon_log.debug(return_messages) return return_messages def admin(self, ctx, msg, rcon_server, admin_roles): player = msg.split(' ||| ')[1].split(' (')[0] con = rcon_con.RconConnection( rcon_server['ip'], rcon_server['port'], rcon_server['password'], True) con.exec_command('ServerChatToPlayer "{0}" GeeksBot: Admin Geeks have been notified you need assistance. Please be patient.'.format(player)) con._sock.close() for role in admin_roles: msg = '{0} {1}'.format(msg,discord.utils.get(ctx.guild.roles, id=admin_roles[role]).mention) return msg @commands.command() @commands.guild_only() async def monitor_chat(self, ctx, *, server=None): '''Begins monitoring the specified ARK server for chat messages and other events. The specified server must already be in the current guild\'s configuration. To add and remove ARK servers from the guild see add_rcon_server and remove_rcon_server. The server argument is not case sensitive and if the server name has 2 words it can be in one of the following forms: first last first_last "first last" To view all the valid ARK servers for this guild see list_ark_servers.''' if checks.is_rcon_admin(self.bot, ctx): if server != None: rcon_connections = json.loads(self.bot.con.one('select rcon_connections from guild_config where guild_id = %(id)s', {'id':ctx.guild.id})) server = server.replace('_',' ').title() if server in rcon_connections: rcon_connections[server]["monitoring_chat"] = 1 self.bot.con.run('update guild_config set rcon_connections = %(json)s where guild_id = %(id)s', {'id':ctx.guild.id, 'json': json.dumps(rcon_connections)}) channel = self.bot.get_channel(rcon_connections[server]['game_chat_chan_id']) await channel.send('Started monitoring on the {0} server.'.format(server)) await ctx.message.add_reaction('✅') rcon_log.debug('Started monitoring on the {0} server.'.format(server)) while rcon_connections[server]["monitoring_chat"] == 1: try: con = rcon_con.RconConnection( rcon_connections[server]['ip'], rcon_connections[server]['port'], rcon_connections[server]['password'], True) messages = await self.bot.loop.run_in_executor(None, self.server_chat_background_process, ctx.guild.id, con) con._sock.close() except TimeoutError: rcon_log.error(traceback.format_exc()) await channel.send('TimeoutError') await asyncio.sleep(30) else: rcon_log.debug('Got chat from {0}.'.format(server)) for message in messages: rcon_log.info(message) message = '```{0}```'.format(message) for command in game_commands: prefix_command = '{0}{1}'.format(game_prefix,command) if prefix_command in message: try: func = getattr(self, command) except AttributeError: rcon_log.warning('Function not found "{0}"'.format(command)) else: rcon_log.info(f'Sending to {command}') message = func(ctx, message, rcon_connections['server']) await channel.send('{0}'.format(message)) await asyncio.sleep(1) rcon_connections = json.loads(self.bot.con.one('select rcon_connections from guild_config where guild_id = %(id)s', {'id':ctx.guild.id})) await channel.send('Monitoring Stopped') else: await ctx.send(f'Server not found: {server}') else: await ctx.send(f'You must include a server in this command.') else: await ctx.send(f'You are not authorized to run this command.') @commands.command() @commands.guild_only() async def end_monitor_chat(self, ctx, *, server=None): '''Ends chat monitoring on the specified server. Context is the same as monitor_chat''' if checks.is_rcon_admin(self.bot, ctx): if server != None: rcon_connections = json.loads(self.bot.con.one('select rcon_connections from guild_config where guild_id = %(id)s', {'id':ctx.guild.id})) server = server.replace('_',' ').title() if server in rcon_connections: rcon_connections[server]["monitoring_chat"] = 0 self.bot.con.run('update guild_config set rcon_connections = %(json)s where guild_id = %(id)s', {'id':ctx.guild.id, 'json': json.dumps(rcon_connections)}) else: await ctx.send(f'Server not found: {server}') else: await ctx.send(f'You must include a server in this command.') else: await ctx.send(f'You are not authorized to run this command.') @commands.command() @commands.guild_only() async def listplayers(self, ctx, *, server=None): '''Lists the players currently connected to the specified ARK server. The specified server must already be in the current guild\'s configuration. To add and remove ARK servers from the guild see add_rcon_server and remove_rcon_server. The server argument is not case sensitive and if the server name has 2 words it can be in one of the following forms: first last first_last "first last" To view all the valid ARK servers for this guild see list_ark_servers.''' if checks.is_rcon_admin(self.bot, ctx): rcon_connections = json.loads(self.bot.con.one('select rcon_connections from guild_config where guild_id = %(id)s', {'id':ctx.guild.id})) if server != None: server = server.replace('_',' ').title() if server in rcon_connections: connection_info = rcon_connections[server] msg = await ctx.send('Getting Data for the {0} server'.format(server.title())) await ctx.channel.trigger_typing() message = self._listplayers(connection_info) await ctx.channel.trigger_typing() await msg.delete() await ctx.send('Players currently on the {0} server:\n{1}'.format(server.title(), message)) else: await ctx.send('That server is not in my configuration.\nPlease add it via !add_rcon_server "{0}" "ip" port "password" if you would like to get info from it.'.format(server)) else: for server in rcon_connections: try: connection_info = rcon_connections[server] msg = await ctx.send('Getting Data for the {0} server'.format(server.title())) async with ctx.channel.typing(): message = self._listplayers(connection_info) except Exception as e: await msg.delete() await ctx.send(f'Player listing failed on the {server.title()}\n{e}') else: await msg.delete() await ctx.send('Players currently on the {0} server:\n{1}'.format(server.title(), message)) else: await ctx.send(f'You are not authorized to run this command.') @commands.command() @commands.guild_only() async def add_rcon_server(self, ctx, server, ip, port, password): '''Adds the specified server to the current guild\'s rcon config. All strings (, , ) must be contained inside double quotes.''' if checks.is_rcon_admin(self.bot, ctx): server = server.title() rcon_connections = json.loads(self.bot.con.one('select rcon_connections from guild_config where guild_id = %(id)s', {'id':ctx.guild.id})) if server not in rcon_connections: rcon_connections[server] = { 'ip': ip, 'port': port, 'password': password, 'name': server.lower().replace(' ','_'), 'game_chat_chan_id': 0, 'msg_chan_id': 0, 'monitoring_chat': 0 } self.bot.con.run('update guild_config set rcon_connections = %(connections)s where guild_id = %(id)s', {'id':ctx.guild.id, 'connections':json.dumps(rcon_connections)}) await ctx.send('{0} server has been added to my configuration.'.format(server)) else: await ctx.send('This server name is already in my configuration. Please choose another.') else: await ctx.send(f'You are not authorized to run this command.') await ctx.message.delete() await ctx.send('Command deleted to prevent password leak.') @commands.command() @commands.guild_only() async def remove_rcon_server(self, ctx, server): '''removes the specified server from the current guild\'s rcon config. All strings must be contained inside double quotes.''' if checks.is_rcon_admin(self.bot, ctx): server = server.title() rcon_connections = json.loads(self.bot.con.one('select rcon_connections from guild_config where guild_id = %(id)s', {'id':ctx.guild.id})) if server in rcon_connections: del rcon_connections[server] self.bot.con.run('update guild_config set rcon_connections = %(connections)s where guild_id = %(id)s', {'id':ctx.guild.id, 'connections':json.dumps(rcon_connections)}) await ctx.send('{0} has been removed from my configuration.'.format(server)) else: await ctx.send('{0} is not in my configuration.'.format(server)) else: await ctx.send(f'You are not authorized to run this command.') @commands.command() @commands.guild_only() async def add_whitelist(self, ctx, *, steam_ids=None): '''Adds the included Steam 64 IDs to the running whitelist on all the ARK servers in the current guild\'s rcon config. Steam 64 IDs should be a comma seperated list of IDs. Example: 76561198024193239,76561198024193239,76561198024193239''' if checks.is_rcon_admin(self.bot, ctx): if steam_ids != None: rcon_connections = json.loads(self.bot.con.one('select rcon_connections from guild_config where guild_id = %(id)s', {'id':ctx.guild.id})) error = 0 error_msg = '' success_msg = 'Adding to the running whitelist on all servers.' steam_ids = steam_ids.replace(', ',',').replace(' ',',').split(',') for (i, steam_id) in enumerate(steam_ids): try: steam_id = int(steam_id) except ValueError: error = 1 error_msg = '{0}\n__**ERROR:**__ {1} is not a valid Steam64 ID'.format(error_msg, steam_id) else: steam_ids[i] = steam_id if error == 0: msg = await ctx.send(success_msg) for server in rcon_connections: try: success_msg = '{0}\n\n{1}:'.format(success_msg, server.title()) await msg.edit(content=success_msg.strip()) messages = await self.bot.loop.run_in_executor(None, self._whitelist, rcon_connections[server], steam_ids) except Exception as e: success_msg = '{0}\n{1}'.format(success_msg, e.strip()) await msg.edit(content=success_msg.strip()) else: for message in messages: success_msg = '{0}\n{1}'.format(success_msg, message.strip()) await msg.edit(content=success_msg.strip()) await msg.add_reaction('✅') else: await ctx.send(error_msg) else: await ctx.send('I need a list of steam IDs to add to the whitelist.') else: await ctx.send(f'You are not authorized to run this command.') @commands.command() @commands.guild_only() async def saveworld(self, ctx, *, server=None): '''Runs SaveWorld on the specified ARK server. If a server is not specified it will default to running saveworld on all servers in the guild\'s config. Will print out "World Saved" for each server when the command completes sucessfully.''' if checks.is_rcon_admin(self.bot, ctx): rcon_connections = json.loads(self.bot.con.one('select rcon_connections from guild_config where guild_id = %(id)s', {'id':ctx.guild.id})) success_msg = 'Running saveworld' if server == None: success_msg += ' on all the servers:' # server = server.replace('_',' ').title() if server in rcon_connections: connection_info = rcon_connections[server] msg = await ctx.send(success_msg) for server in rcon_connections: try: success_msg = '{0}\n\n{1}:'.format(success_msg, server.title()) await msg.edit(content=success_msg.strip()) message = await self.bot.loop.run_in_executor(None, self._saveworld, rcon_connections[server]) except Exception as e: success_msg = '{0}\n{1}'.format(success_msg, e.strip()) await msg.edit(content=success_msg.strip()) else: success_msg = '{0}\n{1}'.format(success_msg, message.strip()) await msg.edit(content=success_msg.strip()) await msg.add_reaction('✅') elif server.title() in rcon_connections: success_msg = '{0} {1}:'.format(success_msg, server.title()) msg = await ctx.send(success_msg) message = await self.bot.loop.run_in_executor(None, self._saveworld, rcon_connections[server.title()]) success_msg = '{0}\n{1}'.format(success_msg, message.strip()) await msg.edit(content=success_msg.strip()) await msg.add_reaction('✅') else: await ctx.send(f'{server.title()} is not currently in the configuration for this guild.') else: await ctx.send(f'You are not authorized to run this command.') @commands.group(case_insensitive=True) async def broadcast(self, ctx): '''Run help broadcast for more info''' pass @broadcast.command(name='all') @commands.guild_only() async def broadcast_all(self, ctx, *, message=None): '''Sends a broadcast message to all servers in the guild config. The message will be prefixed with the Discord name of the person running the command. Will print "Success" for each server once the broadcast is sent.''' if checks.is_rcon_admin(self.bot, ctx): rcon_connections = json.loads(self.bot.con.one('select rcon_connections from guild_config where guild_id = %(id)s', {'id':ctx.guild.id})) if message != None: message = f'{ctx.author.display_name}: {message}' success_msg = f'Broadcasting "{message}" to all servers.' msg = await ctx.send(success_msg) for server in rcon_connections: try: success_msg = '{0}\n\n{1}:'.format(success_msg, server.title()) await msg.edit(content=success_msg.strip()) messages = await self.bot.loop.run_in_executor(None, self._broadcast, rcon_connections[server], message) except Exception as e: success_msg = '{0}\n{1}'.format(success_msg, e.strip()) await msg.edit(content=success_msg.strip()) else: for mesg in messages: if mesg == 'Server received, But no response!!': mesg = 'Success' success_msg = '{0}\n{1}'.format(success_msg, mesg.strip()) await msg.edit(content=success_msg.strip()) await msg.add_reaction('✅') else: await ctx.send('You must include a message with this command.') else: await ctx.send(f'You are not authorized to run this command.') @broadcast.command(name='server') @commands.guild_only() async def broadcast_server(self, ctx, server, *, message=None): '''Sends a broadcast message to the specified server that is in the guild's config. The message will be prefixed with the Discord name of the person running the command. If has more than one word in it's name it will either need to be sorrounded by double quotes or the words seperated by _''' if checks.is_rcon_admin(self.bot, ctx): rcon_connections = json.loads(self.bot.con.one('select rcon_connections from guild_config where guild_id = %(id)s', {'id':ctx.guild.id})) if server != None: server = server.replace('_',' ').title() if message != None: message = f'{ctx.author.display_name}: {message}' success_msg = f'Broadcasting "{message}" to {server}.' msg = await ctx.send(success_msg) if server in rcon_connections: messages = await self.bot.loop.run_in_executor(None, self._broadcast, rcon_connections[server], message) for mesg in messages: if mesg != 'Server received, But no response!!': success_msg = '{0}\n{1}'.format(success_msg, mesg.strip()) await msg.edit(content=success_msg.strip()) await msg.add_reaction('✅') else: await ctx.send(f'{server} is not in the config for this guild') else: await ctx.send('You must include a message with this command.') else: await ctx.send('You must include a server with this command') else: await ctx.send(f'You are not authorized to run this command.') @commands.command() @commands.guild_only() async def create_server_chat_chans(self, ctx): '''Creates a category and server chat channels in the current guild. The category will be named "Server Chats" and read_messages is disabled for the guild default role (everyone) This can be overridden by modifying the category's permissions. Inside this category a channel will be created for each server in the current guild's rcon config and these channel's permissions will be synced to the category. These channels will be added to the guild's rcon config and are where the server chat messages will be sent when monitor_chat is run.''' if checks.is_rcon_admin(self.bot, ctx): rcon_connections = json.loads(self.bot.con.one('select rcon_connections from guild_config where guild_id = %(id)s', {'id':ctx.guild.id})) edited = 0 category = discord.utils.get(ctx.guild.categories, name='Server Chats') if category == None: overrides = {ctx.guild.default_role: discord.PermissionOverwrite(read_messages=False)} category = await ctx.guild.create_category('Server Chats', overwrites = overrides) channels = ctx.guild.channels cat_chans = [] for channel in channels: if channel.category_id == category.id: cat_chans.append(channel) for server in rcon_connections: exists = 0 if cat_chans != []: for channel in cat_chans: if rcon_connections[server]['game_chat_chan_id'] == channel.id: exists = 1 if exists == 0: print('Creating {}'.format(server)) chan = await ctx.guild.create_text_channel(rcon_connections[server]['name'],category=category) rcon_connections[server]['game_chat_chan_id'] = chan.id edited = 1 if edited == 1: self.bot.con.run('update guild_config set rcon_connections = %(json)s where guild_id = %(id)s', {'id':ctx.guild.id, 'json': json.dumps(rcon_connections)}) await ctx.message.add_reaction('✅') else: await ctx.send(f'You are not authorized to run this command.') @commands.command(aliases=['servers','list_servers']) @commands.guild_only() @commands.check(checks.is_restricted_chan) async def list_ark_servers(self, ctx): '''Returns a list of all the ARK servers in the current guild\'s config.''' servers = json.loads(self.bot.con.one('select rcon_connections from guild_config where guild_id = %(id)s', {'id':ctx.guild.id})) em = discord.Embed( style='rich', title=f'__**There are currently {len(servers)} ARK servers in my config:**__', color=discord.Colour.green() ) if ctx.guild.icon: em.set_thumbnail(url=f'{ctx.guild.icon_url}') for server in servers: description = f"឵ **IP:** {servers[server]['ip']}:{servers[server]['port']}\n឵ **Steam Connect:** [steam://connect/{servers[server]['ip']}:{servers[server]['port']}](steam://connect/{servers[server]['ip']}:{servers[server]['port']})" em.add_field(name=f'__***{server}***__', value=description, inline=False) await ctx.send(embed=em) def setup(bot): bot.add_cog(rcon(bot))