import discord, logging, re, sys, traceback, asyncio, datetime, os, time from minecraft_manager.models import Application, Player from minecraft_manager.api import api from django.contrib.auth.models import User from django.conf import settings from django.db import close_old_connections from threading import Thread logger = logging.getLogger(__name__) class Discord(discord.Client): discord_game = 'MCM' prefix = getattr(settings, 'DISCORD_BOT_PREFIX', '!') auth_roles = getattr(settings, 'DISCORD_BOT_ROLES', []) error_users = getattr(settings, 'DISCORD_ERROR_USERS', []) token = None def __init__(self, token, **kwargs): super().__init__(**kwargs) self.token = token @asyncio.coroutine def on_ready(self): print('Logged in as') print(self.user.name) print(self.user.id) print(discord.__version__) print('Voice Loaded: {0}'.format(discord.opus.is_loaded())) print('OAuth URL: https://discordapp.com/api/oauth2/authorize?client_id={0}&permissions=0&scope=bot'.format(self.user.id)) print('------') logger.info('Logged in as {0} ({1}) with discord.py v{2}'.format(self.user.name, self.user.id, discord.__version__)) yield from self.change_presence(game=discord.Game(name=self.discord_game)) @asyncio.coroutine def discord_message(self, channel, message): if isinstance(message, discord.Embed): for idx, field in enumerate(message.fields): if not field.value: message.set_field_at(idx, name=field.name, value="N/A") yield from self.send_message(channel, embed=message) else: yield from self.send_message(channel, message) @asyncio.coroutine def on_message(self, message): # IGNORE DM AND BOTS if message.author.bot is True or message.channel.is_private is True: return member_roles = [role.id for role in message.author.roles] # IF MEMBER IS NOT AUTHORIZED, IGNORE if not any(role in self.auth_roles for role in member_roles): return # FIX STALE DB CONNECTIONS close_old_connections() # HELP match = re.match("[{0}]help$".format(self.prefix), message.content) if match: embed = discord.Embed(colour=discord.Colour(0x417505)) embed.set_thumbnail(url="https://cdn.discordapp.com/avatars/454457830918062081/b5792489bc43d9e17b8f657880a17dd4.png") embed.add_field(name="Minecraft Manager Help", value="-----------------------------") embed.add_field(name="{}[app ]search ".format(self.prefix), value="Search for applications by partial or exact username.") embed.add_field(name="{}[app ]info ".format(self.prefix), value="Get detailed information about a specific application.") embed.add_field(name="{}[app ]accept|deny ".format(self.prefix), value="Take action on an application.") embed.add_field(name="{}demote ".format(self.prefix), value="Demote a player to the role given to accepted applications.") embed.add_field(name="{}compare".format(self.prefix), value="Compare Discord users to the Whitelist.") yield from self.discord_message(message.channel, embed) # APP COMMANDS WITH APP ID match = re.match("[{0}](?:app )?(i|info|a|accept|d|deny) (\d+)$".format(self.prefix), message.content) if match: if match.group(1) and match.group(2): action = match.group(1)[:1] action_display = "accept" if action == "a" else "deny" if action == "d" else "" yield from self.discord_message(message.channel, "{0} App ID **{1}**".format( "Retrieving info for" if action == "i" else "{0}ing".format(action_display.capitalize()), match.group(2))) application = None try: application = Application.objects.get(id=match.group(2)) except: yield from self.discord_message(message.channel, "An Application with that ID doesn't exist.") return if action == "i": # Info msg = self.build_info(application) else: # Action accept = True if action == "a" else False if not application.accepted: application.accepted = accept application.save() msg = "App ID **{0}** was successfully {1}.".format(match.group(2), "accepted" if accept else "denied") api.plugin("accept" if accept else "deny", application.username) else: msg = "App ID **{0}** was already {1}.".format(match.group(2), "accepted" if application.accepted else "denied") yield from self.discord_message(message.channel, msg) return # APP INFO WITH PARTIAL NAME SEARCH match = re.match("[{0}](?:app )?(?:search|info) (\S+)?$".format(self.prefix), message.content) if match: search = match.group(1) yield from self.discord_message(message.channel, "Searching for applications whose username contains '{0}'".format(search)) applications = Application.objects.filter(username__icontains=search)[:10] count = Application.objects.filter(username__icontains=search).count() if count > 0: if count == 1: info = self.build_info(applications[0]) else: info = "**Found the following applications**" for app in applications: info += "\n{0} - {1} ({2})".format(app.id, app.username.replace("_", "\\_"), app.status) if count > 10: info += "\n**This is only 10 applications out of {0} found. Please narrow your search if possible.**".format( len(applications)) else: players = Player.objects.filter(username__icontains=search, application__isnull=False)[:10] count = Player.objects.filter(username__icontains=search, application__isnull=False).count() if count > 0: if count == 1: yield from self.discord_message(message.channel, "No applications matched, however there is a player match.") info = self.build_info(players[0].application) else: info = "No applications matched, however there are player matches." for player in players: app = player.application info += "\n{0} - {1} AKA {2} ({3})".format(app.id, app.username.replace("_", "\\_"), player.username.replace("_", "\\_"), app.status) if count > 10: info += "\n**This is only 10 players out of {0} found. Please narrow your search if possible.**".format( len(players)) else: info = "No applications matched that search." yield from self.discord_message(message.channel, info) # DEMOTE A PLAYER TO MEMBER match = re.match("[{0}]demote (\w+)$".format(self.prefix), message.content) if match: yield from self.delete_message(message) username = match.group(1) api.plugin(api.PLUGIN_DEMOTE, username) deactivated = "" if User.objects.filter(username__iexact=username).exists(): user = User.objects.get(username__iexact=username) user.is_active = False user.save() deactivated = " and de-activated" yield from self.discord_message(message.channel, "{} has been demoted{}.".format(username, deactivated)) # COMPARE DISCORD USERS TO WHITELIST match = re.match("[{0}]compare".format(self.prefix), message.content) if match: yield from self.delete_message(message) yield from self.send_typing(message.channel) no_player = [] no_application = [] for member in message.server.members: if member.bot: continue name = member.nick if member.nick else member.name try: Player.objects.get(username__iexact=name) except: no_player.append(name) try: Application.objects.get(username__iexact=name) except: no_player = no_player[:-1] no_application.append(name) header = "**The following users have an application match, but no player match on the whitelist:**\n" yield from self.discord_message(message.author, "{}```{}```".format(header, "\n".join(no_player))) header = "**The following users do not have an application or player match on the whitelist:**\n" yield from self.discord_message(message.author, "{}```{}```".format(header, "\n".join(no_application))) def build_info(self, application): embed = discord.Embed(colour=discord.Colour(0x417505)) embed.set_thumbnail( url="https://minotar.net/helm/{0}/100.png".format(application.username)) embed.add_field(name="Application ID", value=application.id) embed.add_field(name="Username", value=application.username.replace("_", "\\_")) embed.add_field(name="Age", value=application.age) embed.add_field(name="Type of Player", value=application.player_type) embed.add_field(name="Ever been banned", value=application.ever_banned) if application.ever_banned: embed.add_field(name="Reason for being banned", value=application.ever_banned_explanation) embed.add_field(name="Reference", value=application.reference) embed.add_field(name="Read the Rules", value=application.read_rules) embed.add_field(name="Date", value=application.date_display) embed.add_field(name="Status", value=application.status) return embed @asyncio.coroutine def on_error(self, event, *args, **kwargs): print(sys.exc_info()) print("Exception raised by " + event) error = '{0}\n{1}'.format(sys.exc_info()[1], ''.join(traceback.format_tb(sys.exc_info()[2]))) logger.error(error) for user in self.error_users: try: user = discord.User(id=user) yield from self.discord_message(user, '```python\n{}```'.format(error)) except: pass def run_bot(self): self.run(self.token) class OreAlert: # Options log = os.path.join(settings.MINECRAFT_BASE_DIR, 'logs/latest.log') purge = 30 # How long until we purge, in minutes rotate = 5 # How long without input before we assume the log has rotated notify_start = 5 # How many veins found within the above purge minutes to notify notify_each = 1 # After the initial alert, how many should be found in addition before more alerts? notify_ping = 5 # After the initial alert, how many should be found in addition before more pings? playerList = [] class Player: def __init__(self, name, time, prev=[]): self.name = name self.time = time self.prev = prev def compare(self, player): if self.name == player.name: return True else: return False def to_string(self): return str(self.name) + ": " + str(self.time) + ", previous: " + self.prev_to_string() def prev_to_string(self): ret = "" if len(self.prev) > 0: for p in self.prev: ret += str(p) + " " return ret.rstrip() else: return ret def minute_interval(self, start, end): reverse = False if start > end: start, end = end, start reverse = True delta = (end.hour - start.hour) * 60 + end.minute - start.minute + (end.second - start.second) / 60.0 if reverse: delta = 24 * 60 - delta return delta def follow(self, filename): thefile = open(filename, 'r', encoding='utf-8') thefile.seek(0, 2) # Go to the end of the file start = datetime.datetime.now() end = datetime.datetime.now() api.discord_notification('OreAlert has started successfully.') while True: line = thefile.readline() if not line: if self.minute_interval(start, end) > self.rotate: thefile.close() time.sleep(5) thefile = open(filename, 'r', encoding='utf-8') thefile.seek(0, 2) # Go to the end of the file start = datetime.datetime.now() end = datetime.datetime.now() # api.discord_notification('OreAlert has closed and re-opened the log...hopefully it just rotated.') continue end = end + datetime.timedelta(milliseconds=100) time.sleep(0.1) # Sleep briefly continue start = datetime.datetime.now() end = datetime.datetime.now() yield line def run_bot(self): cur_line = "" try: loglines = self.follow(self.log) for line in loglines: # [00: 03:46] [Server thread / INFO]: [MinecraftManager]: [OreAlert]: Etzelia if "MinecraftManager" in line and "OreAlert" in line: # Filter out non-OreAlert log statements cur_time = line.split()[0].replace("[", "").replace("]", "") if ":" in cur_time: # Make sure we have a time. time_array = cur_time.split(":") name = line.split()[-1] dt = datetime.time(int(time_array[0]), int(time_array[1]), int(time_array[2])) p = self.Player(name, dt) new_player = True for player in self.playerList: if p.compare(player): new_player = False player.prev[:] = [x for x in player.prev if not self.minute_interval(x, dt) > self.purge] # First, purge any times older than our configured amount player.prev.append(dt) # Add the new time if len(player.prev) >= self.notify_start: # MCM Alert if len(player.prev) == self.notify_start: api.create_alert("OreAlert: {0}".format(player.name)) if len(player.prev) % self.notify_each == 0: # In-game Notification api.plugin(api.PLUGIN_STAFF_CHAT, "OreAlert {0} has found {1} diamond ore veins within {2} minutes.".format( player.name, len(player.prev), self.purge)) # Discord Notification ping = True if len(player.prev) % self.notify_ping == 0 else False api.discord_notification( '{0} has found {1} diamond ore veins within {2} minutes.'.format( player.name.replace("_", "\\_"), len(player.prev), self.purge), ping=ping) if new_player: p.prev = [p.time] self.playerList.append(p) except KeyboardInterrupt: api.discord_notification("OreAlert has been stopped manually.") except: api.discord_notification("OreAlert has crashed!", ping=True)