import asyncio import logging import traceback import threading from enum import Enum import discord from discord.ext import commands from django.conf import settings from django.shortcuts import reverse from minecraft_manager.models import Application, Ticket from minecraft_manager.utils import url_path logger = logging.getLogger(__name__) discord_loop = None discord_bot = None description = ''' A Discord bot connected to an MCM instance. ''' class DiscordStatus(Enum): STOPPED = 0 STARTING = 1 STARTED = 2 STOPPING = 3 def __str__(self): return self.name.title() def is_running(self): return self != DiscordStatus.STOPPED discord_status = DiscordStatus.STOPPED class Discord(commands.Bot): discord_game = 'MCM' prefix = getattr(settings, 'DISCORD_BOT_PREFIX', '!') auth_roles = getattr(settings, 'DISCORD_BOT_ROLES', []) superuser_roles = getattr(settings, 'DISCORD_SUPERUSER_ROLES', []) error_users = getattr(settings, 'DISCORD_ERROR_USERS', []) def __init__(self, token, loop): super().__init__(command_prefix=self.prefix, description=description, case_insensitive=True, help_command=None, activity=discord.Game(name=self.discord_game), loop=loop) self.token = token self.load_extension("minecraft_manager.bot.commands") async def on_ready(self): global discord_status discord_status = DiscordStatus.STARTED 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('------') print('Logged in as {0} ({1}) with discord.py v{2}'.format(self.user.name, self.user.id, discord.__version__)) channel_id = getattr(settings, 'DISCORD_MCM_CHANNEL', None) if channel_id: channel = self.get_channel(channel_id) embed = discord.Embed(color=8311585) content = "" unanswered_applications = Application.objects.filter(accepted=None) if len(unanswered_applications) > 0: link = url_path(settings.MCM_BASE_LINK, 'dashboard/application') content += "[Unanswered Applications: {}]({})".format(len(unanswered_applications), link) unclaimed_tickets = Ticket.objects.filter(staff=None, resolved=False) if len(unclaimed_tickets) > 0: link = url_path(settings.MCM_BASE_LINK, 'dashboard/ticket') if content: content += "\n\n" content += "[Unclaimed Tickets: {}]({})".format(len(unclaimed_tickets), link) if content: embed.title = "MCM Reminder" embed.description = content await self.discord_message(channel, embed) async def on_disconnect(self): global discord_status discord_status = DiscordStatus.STOPPED async def discord_message(self, dest, 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") await dest.send(embed=message) else: await dest.send(message) async def on_command_error(self, context, exception): if not isinstance(exception, commands.CommandInvokeError): return if hasattr(exception, "original"): error = ''.join(traceback.format_tb(exception.original.__traceback__)) else: error = exception logger.error(error) for user_id in self.error_users: user = self.get_user(user_id) if user: await self.discord_message(user, '```python\n{}```'.format(error)) def run_bot(self): global discord_loop try: discord_loop.run_until_complete(self.start(self.token)) except KeyboardInterrupt: logger.info("Bot received keyboard interrupt") except Exception as e: print(e) logger.info('Bot encountered the following unhandled exception %s', e) def start(): global discord_loop, discord_bot, discord_status if discord_status != DiscordStatus.STOPPED: return token = getattr(settings, 'DISCORD_BOT_TOKEN', None) discord_loop = asyncio.new_event_loop() discord_bot = Discord(token, discord_loop) thread = threading.Thread(target=discord_bot.run_bot) thread.start() discord_status = DiscordStatus.STARTING def stop(): global discord_loop, discord_bot, discord_status if discord_status == DiscordStatus.STARTED: discord_loop.create_task(discord_bot.close()) discord_status = DiscordStatus.STOPPING discord_loop = None discord_bot = None def restart(): def _restart(): stop() while discord_status.is_running(): pass start() if discord_status != DiscordStatus.STARTED: return thread = threading.Thread(target=_restart) thread.start() def status(): return discord_status.is_running() def display(): return str(discord_status) class DestType(Enum): CHANNEL = 1 USER = 2 def send(dest_type: DestType, dest_id: int, message: str = "", embed: discord.Embed = None): async def _send(): if dest_type == DestType.CHANNEL: dest = discord_bot.get_channel(dest_id) elif dest_type == DestType.USER: dest = discord_bot.get_user(dest_id) else: return if message is not None: await dest.send(message) if embed is not None: for idx, field in enumerate(embed.fields): if not field.value: embed.set_field_at(idx, name=field.name, value="N/A") await dest.send(embed=embed) global discord_loop, discord_bot if discord_loop: discord_loop.create_task(_send())