async def logs(self, ctx, category: str = None): inst = self.bot.cache.instance(ctx.author, ctx.guild.id) await self._query(inst) if category != None: category = category.lower() if category in ["export", "file"]: inst_name = Instance(inst.id).name log_file = discord.File(fp=ServerLogs(inst.id).export(), filename=f"{inst_name}_log.txt") await ctx.send(file=log_file) else: logs = ServerLogs(inst.id).get_logs(category) if not logs: await ctx.send( f"{config.get('error_message_emoji')} No logs could be found!" ) return output = "" for log in logs[::-1]: message = format_log(log) + "\n" if len(message + output) + 12 > 2000: break else: output = message + output output = "```json\n" + output + "```" await ctx.send(output)
async def chat(self, ctx): inst = self.bot.cache.instance(ctx.author, ctx.guild.id) await self._query(inst) messages = ServerLogs(inst.id).get_logs('chat') if not messages: await ctx.send( f"{config.get('error_message_emoji')} There aren't any recent messages!" ) return output = "" for log in messages[::-1]: timestamp = log['timestamp'] time = timestamp.strftime("%H:%M") content = log['message'] message = f"[{time}] {content}\n" if len(message + output) + 12 > 2000: break elif (datetime.now() - timestamp).total_seconds() > 60 * 60 * 24: break else: output = message + output output = "```json\n" + output + "```" await ctx.send(output)
async def disband_squad(self, ctx, team_id: int, squad_id: int, *, reason: str = None): if team_id not in [1, 2]: raise commands.BadArgument('team_id needs to be either 1 or 2') inst = self.bot.cache.instance(ctx.author, ctx.guild.id).update() if team_id == 1: team = inst.team1 elif team_id == 2: team = inst.team2 squads = team.squads try: squad = [squad for squad in squads if squad.id == squad_id][0] except: raise commands.BadArgument('No squad found with this ID') players = inst.select(team_id=team_id, squad_id=squad_id) res = inst.rcon.disband_squad(team_id, squad_id) warn_reason = "An Admin disbanded the squad you were in." if reason: warn_reason = warn_reason + " Reason: " + reason for player in players: inst.rcon.warn(player.steam_id, warn_reason) embed = base_embed(inst.id, title="Squad disbanded", description=res) await ctx.send(embed=embed) ServerLogs(inst.id).add( 'rcon', f'{ctx.author.name}#{ctx.author.discriminator} disbanded {team.faction_short}/Squad{squad_id} for "{reason}"' )
async def kick_from_squad(self, ctx, name_or_id: str, *, reason: str = None): inst = self.bot.cache.instance(ctx.author, ctx.guild.id).update() player = inst.get_player(name_or_id) if not player: raise commands.BadArgument("Player %s isn't online at the moment" % name_or_id) res = inst.rcon.remove_from_squad(player.steam_id) if "is not in a squad" in res: raise RconCommandError(res) warn_reason = "An Admin removed you from your squad." if reason: warn_reason = warn_reason + " Reason: " + reason inst.rcon.warn(player.steam_id, warn_reason) embed = base_embed(inst.id, title="Player removed from squad", description=res) await ctx.send(embed=embed) ServerLogs(inst.id).add( 'rcon', f'{ctx.author.name}#{ctx.author.discriminator} squad-kicked {player.name} for "{reason}"' )
async def broadcast(self, ctx, *, message: str): inst = self.bot.cache.instance(ctx.author, ctx.guild.id) res = inst.rcon.broadcast(message) embed = base_embed(self.bot.cache.instance(ctx.author, ctx.guild.id).id, title="Message broadcasted", description=res) await ctx.send(embed=embed) ServerLogs(inst.id).add( 'rcon', f'{ctx.author.name}#{ctx.author.discriminator} broadcasted "{message}"' )
async def restart_match(self, ctx): inst = self.bot.cache.instance(ctx.author, ctx.guild.id) res = inst.rcon.restart_match() embed = base_embed(self.bot.cache.instance(ctx.author, ctx.guild.id).id, title="Restarted the current match", description=res) await ctx.send(embed=embed) ServerLogs(inst.id).add( 'rcon', f'{ctx.author.name}#{ctx.author.discriminator} restarted the current match' )
async def set_next_map(self, ctx, *, map_name: str): inst = self.bot.cache.instance(ctx.author.id, ctx.guild.id) res = inst.rcon.set_next_map(map_name) inst.next_map = map_name embed = base_embed(self.bot.cache.instance(ctx.author.id, ctx.guild.id).id, title="Queued the next map", description=res) await ctx.send(embed=embed) ServerLogs(inst.id).add( 'rcon', f'{ctx.author.name}#{ctx.author.discriminator} queued {map_name}')
async def warn(self, ctx, name_or_id: str, *, reason: str): inst = self.bot.cache.instance(ctx.author, ctx.guild.id).update() player = inst.get_player(name_or_id) if not player: raise commands.BadArgument("Player %s isn't online at the moment" % name_or_id) res = inst.rcon.warn(player.steam_id, reason) embed = base_embed(inst.id, title="Player warned", description=res) await ctx.send(embed=embed) ServerLogs(inst.id).add( 'rcon', f'{ctx.author.name}#{ctx.author.discriminator} warned {player.name} for "{reason}"' )
async def skip_match(self, ctx, *, map_name: str = ""): inst = self.bot.cache.instance(ctx.author.id, ctx.guild.id) if map_name: res = inst.rcon.switch_to_map(map_name) else: res = inst.rcon.end_match() embed = base_embed(self.bot.cache.instance(ctx.author.id, ctx.guild.id).id, title="Skipped the current match", description=res) await ctx.send(embed=embed) ServerLogs(inst.id).add( 'rcon', f'{ctx.author.name}#{ctx.author.discriminator} skipped the current match' )
async def switch_team(self, ctx, name_or_id: str, *, reason: str = None): inst = self.bot.cache.instance(ctx.author, ctx.guild.id).update() player = inst.get_player(name_or_id) if not player: raise commands.BadArgument("Player %s isn't online at the moment" % name_or_id) res = inst.rcon.change_team(player.steam_id) warn_reason = "An Admin switched your team." if reason: warn_reason = warn_reason + " Reason: " + reason inst.rcon.warn(player.steam_id, warn_reason) embed = base_embed(inst.id, title="Player switched", description=res) await ctx.send(embed=embed) ServerLogs(inst.id).add( 'rcon', f'{ctx.author.name}#{ctx.author.discriminator} team-switched {player.name} for "{reason}"' )
async def kick(self, ctx, name_or_id: str, *, reason: str = None): inst = self.bot.cache.instance(ctx.author.id, ctx.guild.id).update() player = inst.get_player(name_or_id) if not player: raise commands.BadArgument("Player %s isn't online at the moment" % name_or_id) if not reason: reason = "Kicked by a moderator" res = inst.rcon.kick(player.steam_id, reason) self.bot.cache.instance(ctx.author.id, ctx.guild.id).disconnect_player(player) embed = base_embed(inst.id, title="Player kicked", description=res) await ctx.send(embed=embed) ServerLogs(inst.id).add( 'rcon', f'{ctx.author.name}#{ctx.author.discriminator} kicked {player.name} for "{reason}"' )
async def switch_squad(self, ctx, team_id: int, squad_id: int, *, reason: str = None): if team_id not in [1, 2]: raise commands.BadArgument('team_id needs to be either 1 or 2') inst = self.bot.cache.instance(ctx.author, ctx.guild.id).update() if team_id == 1: team = inst.team1 team_other = inst.team2 elif team_id == 2: team = inst.team2 team_other = inst.team1 squads = team.squads try: squad = [squad for squad in squads if squad.id == squad_id][0] except: raise commands.BadArgument('No squad found with this ID') players = inst.select(team_id=team_id, squad_id=squad_id) warn_reason = "An Admin switched your team." if reason: warn_reason = warn_reason + " Reason: " + reason for player in players: try: inst.rcon.change_team(player.steam_id) inst.rcon.warn(player.steam_id, warn_reason) except RconCommandError: pass embed = base_embed( inst.id, title=f"Switched {str(len(players))} players", description=f"from {team.faction} to {team_other.faction}") await ctx.send(embed=embed) ServerLogs(inst.id).add( 'rcon', f'{ctx.author.name}#{ctx.author.discriminator} team-switched {", ".join([player.name for player in players])} for "{reason}"' )
async def warn_all(self, ctx, *, reason: str): inst = self.bot.cache.instance(ctx.author, ctx.guild.id).update() amount = 0 for player in inst.players: try: inst.rcon.warn(player.steam_id, reason) except: pass else: amount += 1 res = f"Warned {str(amount)}/{str(len(inst.players))} players for '{reason}'" embed = base_embed(inst.id, title="All players warned", description=res) await ctx.send(embed=embed) ServerLogs(inst.id).add( 'rcon', f'{ctx.author.name}#{ctx.author.discriminator} warned {str(amount)} players for "{reason}"' )
async def set_next_map(self, ctx, *, map_name: str): inst = self.bot.cache.instance(ctx.author, ctx.guild.id) instance_details = Instance( ctx.bot.cache._get_selected_instance(ctx.author, ctx.guild.id)) # check current game for instance select - squad uses another command if instance_details.game == 'squad': res = inst.rcon.set_next_layer(map_name) else: res = inst.rcon.set_next_map(map_name) inst.next_map = Map(map_name) embed = base_embed(self.bot.cache.instance(ctx.author, ctx.guild.id).id, title="Queued the next map", description=res) await ctx.send(embed=embed) ServerLogs(inst.id).add( 'rcon', f'{ctx.author.name}#{ctx.author.discriminator} queued {map_name}')
async def skip_match(self, ctx, *, map_name: str = ""): inst = self.bot.cache.instance(ctx.author, ctx.guild.id) instance_details = Instance( ctx.bot.cache._get_selected_instance(ctx.author, ctx.guild.id)) if map_name: # check current game for instance select - squad uses another command if instance_details.game == 'squad': res = inst.rcon.switch_to_layer(map_name) else: res = inst.rcon.switch_to_map(map_name) else: res = inst.rcon.end_match() embed = base_embed(self.bot.cache.instance(ctx.author, ctx.guild.id).id, title="Skipped the current match", description=res) await ctx.send(embed=embed) ServerLogs(inst.id).add( 'rcon', f'{ctx.author.name}#{ctx.author.discriminator} skipped the current match' )
async def check_server(self): for inst in self.bot.cache.instances.values(): if inst: await self._query(inst) try: max_id = self.last_seen_id[inst.id] except KeyError: max_id = ServerLogs(inst.id)._get_max_log_id() new_max_id = max_id new_logs = [] else: new_max_id, new_logs = ServerLogs( inst.id).get_logs_after(max_id) config = Instance(inst.id).config guild = self.bot.get_guild(config['guild_id']) if guild and new_logs: # Note: Chat logs are handled alongside the triggers channel_joins = guild.get_channel( config['channel_log_joins']) channel_match = guild.get_channel( config['channel_log_match']) channel_rcon = guild.get_channel( config['channel_log_rcon']) if channel_rcon: default_embed = base_embed(inst.id) logs = [ log for log in new_logs if log['category'] == 'rcon' ] for log in logs: embed = default_embed embed.color = discord.Color.teal() embed.title = log['message'] embed.set_footer( text= f"Recorded at {log['timestamp'].strftime('%a, %b %d, %Y %I:%M %p')}" ) await channel_match.send(embed=embed) if channel_joins: default_embed = base_embed(inst.id) logs = [ log for log in new_logs if log['category'] == 'joins' ] if logs: joins = [ log['message'] for log in logs if log['message'].endswith(' connected') ] leaves = [ log['message'] for log in logs if not log['message'].endswith(' connected') ] embed = default_embed embed.set_footer( text= f"Recorded at {logs[-1]['timestamp'].strftime('%a, %b %d, %Y %I:%M %p')}" ) if joins: embed.color = discord.Color.dark_green() embed.description = "\n".join(joins) await channel_match.send(embed=embed) if leaves: embed.color = discord.Embed.Empty embed.description = "\n".join(leaves) await channel_match.send(embed=embed) if channel_match: default_embed = base_embed(inst.id) logs = [ log for log in new_logs if log['category'] == 'match' ] for log in logs: embed = default_embed embed.color = discord.Color.from_rgb(255, 255, 255) embed.title = log['message'] embed.set_footer( text= f"Recorded at {log['timestamp'].strftime('%a, %b %d, %Y %I:%M %p')}" ) await channel_match.send(embed=embed) self.last_seen_id[inst.id] = new_max_id
async def check_server(self): try: for inst in self.bot.cache.instances.values(): if inst: try: await self._query(inst) try: max_id = self.last_seen_id[inst.id] except KeyError: max_id = ServerLogs(inst.id)._get_max_log_id() new_max_id = max_id new_logs = [] else: new_max_id, new_logs = ServerLogs( inst.id).get_logs_after(max_id) config = Instance(inst.id).config guild = self.bot.get_guild(config['guild_id']) if guild and new_logs: # Note: Chat logs are handled alongside the triggers channel_joins = guild.get_channel( config['channel_log_joins']) channel_match = guild.get_channel( config['channel_log_match']) channel_rcon = guild.get_channel( config['channel_log_rcon']) channel_teamkills = guild.get_channel( config['channel_log_teamkills']) if channel_rcon: default_embed = base_embed(inst.id) logs = [ log for log in new_logs if log['category'] == 'rcon' ] for log in logs: embed = default_embed embed.color = discord.Color.teal() embed.title = log['message'] embed.set_footer( text= f"Recorded at {log['timestamp'].strftime('%a, %b %d, %Y %I:%M %p')}" ) await channel_rcon.send(embed=embed) if channel_joins: default_embed = base_embed(inst.id) logs = [ log for log in new_logs if log['category'] == 'joins' ] if logs: joins = [ log['message'] for log in logs if log['message'].endswith(' connected') ] leaves = [ log['message'] for log in logs if not log['message'].endswith( ' connected') ] embed = default_embed embed.set_footer( text= f"Recorded at {logs[-1]['timestamp'].strftime('%a, %b %d, %Y %I:%M %p')}" ) if joins: embed.color = discord.Color.dark_green( ) embed.description = "\n".join(joins) await channel_joins.send(embed=embed) if leaves: embed.color = discord.Embed.Empty embed.description = "\n".join(leaves) await channel_joins.send(embed=embed) if channel_match: default_embed = base_embed(inst.id) logs = [ log for log in new_logs if log['category'] == 'match' ] for log in logs: embed = default_embed embed.color = discord.Color.from_rgb( 255, 255, 255) embed.title = log['message'] embed.set_footer( text= f"Recorded at {log['timestamp'].strftime('%a, %b %d, %Y %I:%M %p')}" ) await channel_match.send(embed=embed) if channel_teamkills: default_embed = base_embed(inst.id) logs = [ log for log in new_logs if log['category'] == 'teamkill' ] if logs: embed = default_embed embed.set_footer( text= f"Recorded at {logs[-1]['timestamp'].strftime('%a, %b %d, %Y %I:%M %p')}" ) embed.color = discord.Color.dark_red() embed.description = "\n".join( [log['message'] for log in logs]) await channel_teamkills.send(embed=embed) self.last_seen_id[inst.id] = new_max_id except Exception as e: logging.exception( 'Inst %s: Unhandled exception whilst updating: %s: %s', inst.id, e.__class__.__name__, e) if isinstance(e, (RconAuthError, ConnectionLost)) and ( datetime.now() - timedelta(minutes=30)) > inst.last_updated: self.bot.cache.instances[inst.id] = None except Exception as e: logging.critical( 'Unhandled exception in instance update cycle: %s: %s', e.__class__.__name__, e)
async def _query(self, inst): # Execute a command to receive new chat packets. # We can use this opportunity to update the cache, # though we don't want to overdo this. if (datetime.now() - inst.last_updated ).total_seconds() > SECONDS_BETWEEN_CACHE_REFRESH: inst.update() else: inst.rcon.exec_command("a") # Grab all the new chat messages new_chat_messages = inst.rcon.get_player_chat() inst.rcon.clear_player_chat() # Parse incoming messages # '[ChatAll] [SteamID:12345678901234567] [FP] Clan Member 1 : Hello world!' # '[ChatAdmin] ASQKillDeathRuleset : Player S.T.A.L.K.E.R%s Team Killed Player NUKE' # '[SteamID:76561198129591637] (WTH) Dylan has possessed admin camera.' for message in new_chat_messages: raw_data = {} if message.startswith('[ChatAdmin] ASQKillDeathRuleset'): # The message is a logged teamkill p1_name, p2_name = re.search( r'\[ChatAdmin\] ASQKillDeathRuleset : Player (.*)%s Team Killed Player (.*)', message).groups() p1 = inst.get_player(p1_name) p2 = inst.get_player(p2_name) p1_output = f"{p1_name} ({p1.steam_id})" if p1 else p1_name p2_output = f"{p2_name} ({p2.steam_id})" if p2 else p2_name message = f"{p1_output} team killed {p2_output}" ServerLogs(inst.id).add("teamkill", message) continue else: try: raw_data['channel'], raw_data['steam_id'], raw_data[ 'name'], raw_data['message'] = re.search( r'\[(.+)\] \[SteamID:(\d{17})\] (.*) : (.*)', message).groups() except: continue player = inst.get_player(int(raw_data['steam_id'])) if player: faction = inst.team1.faction_short if player.team_id == 1 else inst.team2.faction_short squad = player.squad_id if raw_data['channel'] == "ChatAdmin": continue if raw_data['channel'] == "ChatAll": channel = "All" elif raw_data['channel'] == "ChatTeam": channel = faction elif raw_data['channel'] == "ChatSquad": channel = f"{faction}/Squad{str(squad)}" else: channel = raw_data['channel'] else: channel = "Unknown" name = raw_data['name'] text = raw_data['message'] message = f"[{channel}] {name}: {text}" ServerLogs(inst.id).add("chat", message) # Do stuff with new messages instance = Instance(inst.id) config = instance.config guild = self.bot.get_guild(config["guild_id"]) if guild: # Trigger words trigger_channel = guild.get_channel( config["chat_trigger_channel_id"]) trigger_words = config["chat_trigger_words"].split(",") if trigger_channel and trigger_words: trigger_mentions = config["chat_trigger_mentions"] for word in trigger_words: word = word.strip().lower() if word in text.lower(): try: last_attempt = self.trigger_cooldowns[ player.steam_id] cooldown = int( config['chat_trigger_cooldown'] - (datetime.now() - last_attempt).total_seconds()) except KeyError: cooldown = -1 if cooldown == 0: # The player sent multiple admin reports in the same period. # Let's not ping the admins twice, because why would we? trigger_mentions = "" if cooldown > 0: # The player tried sending admin reports too fast and is still in cooldown inst.rcon.warn( player.steam_id, f"You're sending reports too fast! Please wait {str(cooldown)}s and try again." ) description = f"{name}: {discord.utils.escape_markdown(text)}" embed = base_embed(inst.id, description=description) embed.set_footer( text= f"Cooldown was active for this player ({str(cooldown)}s). He was asked to try again." ) await trigger_channel.send(embed=embed) elif len(text.split( )) < 3 and config["chat_trigger_require_reason"]: # The player didn't include a reason, which is required inst.rcon.warn( player.steam_id, f"Please include a reason in your '{word}' report and try again." ) description = f"{name}: {discord.utils.escape_markdown(text)}" embed = base_embed(inst.id, description=description) embed.set_footer( text= "No reason was included in this report. The player was asked to try again." ) await trigger_channel.send(embed=embed) else: self.trigger_cooldowns[ player.steam_id] = datetime.now() description = f"{name}: {discord.utils.escape_markdown(text)}" embed = base_embed(inst.id, title="New Report", description=description, color=discord.Color.red()) if player: embed.set_footer( text= "Team ID: %s • Squad ID: %s • Player ID: %s" % (player.team_id, player.squad_id, player.player_id)) await trigger_channel.send(trigger_mentions, embed=embed) if config['chat_trigger_confirmation']: inst.rcon.warn( player.steam_id, config['chat_trigger_confirmation']) break # Auto log chat_channel = guild.get_channel(config["channel_log_chat"]) if chat_channel: embed = base_embed(instance) title = "{: <20} {}".format("[" + channel + "]", name) embed.add_field(name=title, value=discord.utils.escape_markdown(text)) embed.set_footer( text= f"Recorded at {datetime.now().strftime('%a, %b %d, %Y %I:%M %p')}" ) if not player or channel == "Unknown": embed.color = discord.Embed.Empty elif raw_data['channel'] == "ChatAll": embed.color = discord.Color.dark_gray() elif raw_data['channel'] == "ChatTeam": if player.team_id == 1: embed.color = discord.Color.from_rgb(66, 194, 245) else: embed.color = discord.Color.from_rgb(240, 36, 53) elif raw_data['channel'] == "ChatSquad": if player.team_id == 1: embed.color = discord.Color.from_rgb(0, 100, 255) else: embed.color = discord.Color.from_rgb(201, 4, 24) await chat_channel.send(embed=embed)