diff --git a/api/api.py b/api/api.py index ab98156..fc882af 100644 --- a/api/api.py +++ b/api/api.py @@ -22,48 +22,47 @@ PLUGIN_DEMOTE = 'demote' def plugin(key, command): - host = '127.0.0.1' - port = getattr(settings, 'PLUGIN_PORT', None) - full_command = "{0} {1}".format(key, command) - if port and plugin_exists(): - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.connect((host, port)) - sock.sendall(full_command.encode('utf-8')) - sock.close() + try: + host = '127.0.0.1' + port = getattr(settings, 'PLUGIN_PORT', None) + full_command = "{0} {1}".format(key, command) + if port and plugin_exists(): + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect((host, port)) + sock.sendall(full_command.encode('utf-8')) + sock.close() + return True + except: + pass + return False -def discord_mcm(message='', embeds=None, ping=False): +def discord_mcm(message='', embed=None, ping=False): discord_mcm_webhook = getattr(settings, 'DISCORD_MCM_WEBHOOK', None) if discord_mcm_webhook: - ping_list = getattr(settings, 'DISCORD_PING_LIST', []) - if ping and ping_list: - ping_list = ["<@&{0}>".format(ping) for ping in ping_list] - message = "{0}\n{1}".format(" ".join(ping_list), message) - data = {} - if message: - data['content'] = message - if embeds: - data['embeds'] = embeds - return requests.post(discord_mcm_webhook, json=data) + return post_webhook(discord_mcm_webhook, message, embed, ping) return None -def discord_notification(message='', embeds=None, ping=False): +def discord_notification(message='', embed=None, ping=False): discord_notification_webhook = getattr(settings, 'DISCORD_NOTIFICATION_WEBHOOK', None) if discord_notification_webhook: - ping_list = getattr(settings, 'DISCORD_PING_LIST', []) - if ping and ping_list: - ping_list = ["<@&{0}>".format(ping) for ping in ping_list] - message = "{0}\n{1}".format(" ".join(ping_list), message) - data = {} - if message: - data['content'] = message - if embeds: - data['embeds'] = embeds - return requests.post(discord_notification_webhook, json=data) + return post_webhook(discord_notification_webhook, message, embed, ping) return None +def post_webhook(webhook_url, message, embed, ping): + ping_list = getattr(settings, 'DISCORD_PING_LIST', []) + if ping and ping_list: + ping_list = ["<@&{0}>".format(ping) for ping in ping_list] + message = "{0}\n{1}".format(" ".join(ping_list), message) + data = {} + if message: + data['content'] = message + if embed: + data['embeds'] = [embed.to_dict()] + return requests.post(webhook_url, json=data) + def strip_format(message): return message.replace("§0", "").replace("§1", "").replace("§2", "").replace("§3", "").replace("§4", "") \ diff --git a/api/bot.py b/api/bot.py deleted file mode 100644 index 2f32173..0000000 --- a/api/bot.py +++ /dev/null @@ -1,212 +0,0 @@ -import discord, logging, re, sys, traceback, asyncio -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 - - -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', []) - new_member_roles = getattr(settings, 'DISCORD_BOT_NEW_MEMBER_ROLES', []) - 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] - - # FIX STALE DB CONNECTIONS - close_old_connections() - - - # IF MEMBER IS NOT AUTHORIZED, IGNORE - if not any(role in self.auth_roles for role in member_roles): - return - - # 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 "" - 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() - if Player.objects.filter(username__iexact=application.username).exists(): - player = Player.objects.get(username__iexact=application.username) - player.application_id = application.id - player.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) - 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) - - - diff --git a/api/views.py b/api/views.py index 5b702e4..d1798b5 100644 --- a/api/views.py +++ b/api/views.py @@ -46,7 +46,7 @@ def clean(model, data): attr = d if '__' in d: attr = d.split('__')[0] - if hasattr(model, attr): + if hasattr(model, attr) and attr != "api": cleaned[d] = data[d] return cleaned @@ -138,11 +138,11 @@ class WebAPI(View): def access_level(self, user): access = {'cpp': False, 'cpf': False, 'cpa': False} - if user.has_perm('auth.coreprotect_partial'): + if user.has_perm('minecraft_manager.coreprotect_partial'): access['cpp'] = True - if user.has_perm('auth.coreprotect_full'): + if user.has_perm('minecraft_manager.coreprotect_full'): access['cpf'] = True - if user.has_perm('auth.coreprotect_activity'): + if user.has_perm('minecraft_manager.coreprotect_activity'): access['cpa'] = True return access @@ -192,7 +192,7 @@ class PluginAPI(View): json['message'] = "{0}'s application was submitted.".format(application.username) json['extra'] = application.id msg = mcm_utils.build_application(application) - mcm_api.discord_mcm(message='New Application!', embeds=msg) + mcm_api.discord_mcm(message='New Application!', embed=msg) elif "application_action" == keyword: if Application.objects.filter(id=post['application_id']).exists(): application = Application.objects.get(id=post['application_id']) @@ -279,11 +279,13 @@ class PluginAPI(View): json['message'] = "Updated {0}".format(post['username']) elif "register" == keyword: player = Player.objects.get(uuid=post['uuid']) + password = mcm_api.generate_password() if player.auth_user: - json['status'] = False - json['message'] = "You are already registered. To change your password, contact an Admin." + player.auth_user.password = password + player.auth_user.is_active = True + player.auth_user.save() + json['message'] = password else: - password = mcm_api.generate_password() user = User.objects.create_user(username=player.username.lower(), password=password) user.save() player.auth_user = user @@ -298,7 +300,7 @@ class PluginAPI(View): link = "{}".format(mcm_utils.url_path(settings.MCM_BASE_LINK, 'dashboard/ticket', ticket.id)) msg = mcm_utils.build_ticket(ticket, link) json['extra'] = {'id': ticket.id, 'link': link} - mcm_api.discord_mcm(embeds=msg, ping=True) + mcm_api.discord_mcm(embed=msg, ping=True) except: json['status'] = False json['message'] = "Error while submitting ticket." @@ -311,7 +313,7 @@ class PluginAPI(View): json['message'] = "Warning issued." link = "{}".format(mcm_utils.url_path(settings.MCM_BASE_LINK, 'dashboard/note', warning.id)) msg = mcm_utils.build_warning(warning, link) - mcm_api.discord_mcm(embeds=msg) + mcm_api.discord_mcm(embed=msg) except Exception as ex: json['status'] = False json['message'] = "Error while issuing warning." @@ -396,7 +398,31 @@ class ModelAPI(View): return JsonResponse(json, safe=False) def post(self, request, request_model): - pass + json = {"success": False, "message": ""} + if request_allowed(request, 'model_post_permission'): + post = request.POST + model = None + for m in apps.get_app_config('minecraft_manager').get_models(): + if m._meta.model_name.upper() == request_model.upper(): + model = m + break + if model: + keywords = clean(model, post) + if "id" in keywords: + try: + obj = model.objects.get(id=keywords["id"]) + for key in keywords.keys(): + setattr(obj, key, keywords[key]) + obj.save() + json["success"] = True + json["message"] = "Model updated" + except Exception as ex: + print(ex) + json["message"] = "Could not update model" + else: + json["message"] = "Must provide an ID" + + return JsonResponse(json) class StatsAPI(View): diff --git a/assets/coreprotect/blocks.txt b/assets/coreprotect/blocks.txt deleted file mode 100644 index 0bdca39..0000000 --- a/assets/coreprotect/blocks.txt +++ /dev/null @@ -1,9 +0,0 @@ -You will need to get this from your CP MySQL tables - -NOTE: CoreProtect does not match up these IDs with the block's in-game ID, so air is not 0, stone is not 1, etc. - -1. mysql -p -e "SELECT id, material FROM co_material_map" > blocks.txt -2. Open blocks.txt -3. Remove the header row -4. Remove spaces/tabs and delimit with a ',' -e.g. 1,minecraft:air \ No newline at end of file diff --git a/assets/migrations/001-whitelist.sql b/assets/migrations/001-whitelist.sql deleted file mode 100644 index 268b167..0000000 --- a/assets/migrations/001-whitelist.sql +++ /dev/null @@ -1,55 +0,0 @@ --- Alerts -insert into minecraft_manager_alert (select * from whitelist_alert); - --- Applications -insert into minecraft_manager_application (select * from whitelist_application); - --- Players -insert into minecraft_manager_player (uuid, username, application_id, auth_user_id, last_seen, first_seen) -select wp.uuid, wp.username, ( -select mma.id from minecraft_manager_application mma where mma.username = ( -select wa.username from whitelist_application wa where wp.application_id = wa.id -) -), wp.auth_user_id, wp.last_seen, wp.first_seen from whitelist_player wp -; - --- Tickets -insert into minecraft_manager_ticket (message, priority, resolved, world, x, y, z, date, player_id, staff_id) -select wt.message, wt.priority, wt.resolved, wt.world, wt.x, wt.y, wt.z, wt.date, ( -select mmp.id from minecraft_manager_player mmp where mmp.uuid = ( -select wp.uuid from whitelist_player wp where wp.id = wt.player_id -) -), ( -select au.id from auth_user au where au.username = ( -select wp2.username from whitelist_player wp2 where wp2.id = wt.staff_id -) -) from whitelist_ticket wt -; - --- Warnings -insert into minecraft_manager_warning (message, severity, date, player_id, staff_id) -select ww.message, ww.severity, ww.date, ( -select mmp.id from minecraft_manager_player mmp where mmp.uuid = ( -select wp.uuid from whitelist_player wp where wp.id = ww.player_id -) -), ( -select au.id from auth_user au where au.username = ( -select wp2.username from whitelist_player wp2 where wp2.id = ww.staff_id -) -) from whitelist_warning ww -; - --- User Settings -insert into minecraft_manager_usersettings (default_results, default_theme, default_timezone, search_player_ip, show_timestamp_chat, last_ip, auth_user_id) -select default_results, default_theme, default_timezone, search_player_ip, show_timestamp_chat, last_ip, auth_user_id from whitelist_usersettings wu -; - --- Notes (This migration ONLY works if you are using standard whitelist app, aka only Tickets had notes) --- The ignore is because there were some incorrectly encoded characters giving MySQL a hard time -insert ignore into minecraft_manager_note (ref_table, ref_id, message, last_update, author_id) -select wn.ref_table, ( -select mmt.id from minecraft_manager_ticket mmt where mmt.message = ( -select wt.message from whitelist_ticket wt where wt.id = wn.ref_id -) -), wn.message, wn.last_update, wn.author_id from whitelist_note wn where (select count(*) from whitelist_ticket wt2 where wt2.id = wn.ref_id) > 0 -; \ No newline at end of file diff --git a/bot/__init__.py b/bot/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/assets/bots/Discord-MCM.bot.py b/bot/assets/example.py similarity index 50% rename from assets/bots/Discord-MCM.bot.py rename to bot/assets/example.py index ef882c9..0b660fd 100644 --- a/assets/bots/Discord-MCM.bot.py +++ b/bot/assets/example.py @@ -1,18 +1,29 @@ -import os, sys, django +import os +import sys +import django +# This block is assuming you will use this exact file sep = os.sep path = os.path.dirname(os.path.abspath(__file__)) path = path.split(sep)[:-3] project = path[-1] path = sep.join(path) + +# What you need here is +# project = name of your main django project +# path = path to the root of your django project +# e.g. If your project is at /home/mcm/django1 and settings.py is at /home/mcm/django1/django2/settings.py +# project = django2 +# path = /home/mcm/django1 sys.path.append(path) print("Setting path for {0}: {1}".format(project, path)) os.environ.setdefault("DJANGO_SETTINGS_MODULE", "{}.settings".format(project)) django.setup() from django.conf import settings -from minecraft_manager.api.bot import Discord +from minecraft_manager.bot.discord import Discord token = getattr(settings, 'DISCORD_BOT_TOKEN', None) bot = Discord(token) + bot.run_bot() diff --git a/bot/commands.py b/bot/commands.py new file mode 100644 index 0000000..9d6f4c1 --- /dev/null +++ b/bot/commands.py @@ -0,0 +1,240 @@ +import discord +from discord.ext import commands +from django.contrib.auth.models import User +from django.db import close_old_connections + +from minecraft_manager.api import api +from minecraft_manager.bot.utils import get_application +from minecraft_manager.utils import build_application +from minecraft_manager.models import Application, Player + + +class Commands(commands.Cog): + + def __init__(self, bot): + self.bot = bot + + async def cog_before_invoke(self, ctx): + # FIX STALE DB CONNECTIONS + close_old_connections() + + async def cog_check(self, ctx): + # No DMs + if ctx.guild is None: + return False + + # Check roles + if not hasattr(ctx.author, "roles"): + return False + + for role in ctx.author.roles: + for auth_role in self.bot.auth_roles: + if role.id == auth_role: + return True + return False + + def is_superuser(self, member: discord.Member): + for role in member.roles: + for auth_role in self.bot.superuser_roles: + if role.id == auth_role: + return True + return False + + @commands.command() + async def help(self, ctx): + 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.bot.prefix), + value="Search for applications by partial or exact username.") + embed.add_field(name="{}app info ".format(self.bot.prefix), + value="Get detailed information about a specific application.") + embed.add_field(name="{}app accept|deny ".format(self.bot.prefix), value="Take action on an application.") + embed.add_field(name="{}demote ".format(self.bot.prefix), + value="Demote a player to the role given to accepted applications.") + embed.add_field(name="{}compare".format(self.bot.prefix), value="Compare Discord users to the Whitelist.") + await self.bot.discord_message(ctx.message.channel, embed) + + @commands.group("app", aliases=["application"]) + async def app(self, ctx): + if ctx.invoked_subcommand is None: + await self.bot.discord_message(ctx.message.channel, "No sub-command supplied. Info, Search, Accept, or Deny.") + + @commands.command() + async def info(self, ctx, *args): + await self._info(ctx, *args) + + @app.command("info") + async def app_info(self, ctx, *args): + await self._info(ctx, *args) + + async def _info(self, ctx, *args): + if len(args) == 0: + await self.bot.discord_message(ctx.message.channel, "Info requires an application ID or username.") + + key = args[0] + is_id = True + try: + int(key) + except: + is_id = False + + application = None + if is_id: + application = get_application(key) + if not application: + await self.bot.discord_message(ctx.message.channel, "An Application with that ID doesn't exist.") + return + else: + found = False + applications = Application.objects.filter(username__icontains=key) + if len(applications) == 0: + applications = Application.objects.filter(player__username__icontains=key) + if len(applications) == 1: + await self.bot.discord_message(ctx.message.channel, "**No applications matched, however there is a player match**") + application = applications[0] + found = True + elif len(applications) == 1: + application = applications[0] + found = True + if not found: + await self.bot.discord_message(ctx.message.channel, "An exact Application could not be found. Try search instead.") + return + await self.bot.discord_message(ctx.message.channel, build_application(application)) + + @commands.command() + async def accept(self, ctx, app_id: int): + await self._accept(ctx, app_id) + + @app.command("accept") + async def app_accept(self, ctx, app_id: int): + await self._accept(ctx, app_id) + + async def _accept(self, ctx, app_id: int): + application = get_application(app_id) + if not application: + await self.bot.discord_message(ctx.message.channel, "An Application with that ID doesn't exist.") + return + + if not application.accepted: + application.accepted = True + application.save() + if Player.objects.filter(username__iexact=application.username).exists(): + player = Player.objects.get(username__iexact=application.username) + player.application_id = application.id + player.save() + await self.bot.discord_message(ctx.message.channel, "App ID **{0}** was successfully accepted.".format(app_id)) + if not api.plugin(api.PLUGIN_ACCEPT, application.username): + await self.bot.discord_message(ctx.message.channel, "Could not accept in-game, is the server running?") + + @commands.command() + async def deny(self, ctx, app_id: int): + await self._deny(ctx, app_id) + + @app.command("deny") + async def app_deny(self, ctx, app_id: int): + await self._deny(ctx, app_id) + + async def _deny(self, ctx, app_id: int): + application = get_application(app_id) + if not application: + await self.bot.discord_message(ctx.message.channel, "An Application with that ID doesn't exist.") + return + + if not application.accepted: + application.accepted = False + application.save() + if Player.objects.filter(username__iexact=application.username).exists(): + player = Player.objects.get(username__iexact=application.username) + player.application_id = application.id + player.save() + await self.bot.discord_message(ctx.message.channel, "App ID **{0}** was successfully denied.".format(app_id)) + if not api.plugin(api.PLUGIN_DENY, application.username): + await self.bot.discord_message(ctx.message.channel, "Could not deny in-game, is the server running?") + + @commands.command() + async def search(self, ctx, search: str): + await self._search(ctx, search) + + @app.command("search") + async def app_search(self, ctx, search: str): + await self._search(ctx, search) + + async def _search(self, ctx, search: str): + applications = Application.objects.filter(username__icontains=search)[:10] + count = Application.objects.filter(username__icontains=search).count() + if count > 0: + if count == 1: + info = build_application(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: + await self.bot.discord_message(ctx.message.channel, "**No applications matched, however there is a player match**") + info = build_application(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." + await self.bot.discord_message(ctx.message.channel, info) + + @commands.command() + async def demote(self, ctx, username: str): + if not self.is_superuser(ctx.author): + return + await ctx.message.delete() + if api.plugin(api.PLUGIN_DEMOTE, username): + await self.bot.discord_message(ctx.message.channel, "{} has been demoted in-game.".format(username)) + else: + await self.bot.discord_message(ctx.message.channel, "{} could not be demoted in-game, is the server running?".format(username)) + if User.objects.filter(username__iexact=username).exists(): + user = User.objects.get(username__iexact=username) + user.is_active = False + user.save() + await self.bot.discord_message(ctx.message.channel, "{} has been de-activated in MCM.".format(username)) + else: + await self.bot.discord_message(ctx.message.channel, "{} could not be found in MCM, is their account up-to-date?".format(username)) + + @commands.command() + async def compare(self, ctx): + await ctx.message.delete() + await ctx.message.channel.trigger_typing() + no_player = [] + no_application = [] + for member in ctx.message.guild.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) + if no_player: + header = "**The following users have an application match, but no player match on the whitelist:**\n" + await self.bot.discord_message(ctx.author, "{}```{}```".format(header, "\n".join(no_player))) + if no_application: + header = "**The following users do not have an application or player match on the whitelist:**\n" + await self.bot.discord_message(ctx.author, "{}```{}```".format(header, "\n".join(no_application))) + + +def setup(bot): + bot.add_cog(Commands(bot)) diff --git a/bot/discord.py b/bot/discord.py new file mode 100644 index 0000000..2b57345 --- /dev/null +++ b/bot/discord.py @@ -0,0 +1,95 @@ +import asyncio +import logging +import traceback + +import discord +from discord.ext import commands +from django.conf import settings + +from minecraft_manager.models import Application, Ticket +from minecraft_manager.utils import url_path + +logger = logging.getLogger(__name__) + +description = ''' +A Discord bot connected to an MCM instance. +''' + + +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): + super().__init__(command_prefix=self.prefix, description=description, case_insensitive=True, help_command=None, + activity=discord.Game(name=self.discord_game)) + self.token = token + self.load_extension("minecraft_manager.bot.commands") + + async 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('------') + 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 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): + loop = asyncio.get_event_loop() + try: + 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) + finally: + loop.run_until_complete(self.logout()) + logger.info("Bot shutting down...") diff --git a/bot/utils.py b/bot/utils.py new file mode 100644 index 0000000..93c86d5 --- /dev/null +++ b/bot/utils.py @@ -0,0 +1,8 @@ +from minecraft_manager.models import Application + + +def get_application(app_id): + try: + return Application.objects.get(id=app_id) + except: + return None \ No newline at end of file diff --git a/docs/source/django-settings.rst b/docs/source/django-settings.rst index a7ef2fb..a4ebbd8 100644 --- a/docs/source/django-settings.rst +++ b/docs/source/django-settings.rst @@ -25,11 +25,27 @@ Optional ``DISCORD_NOTIFICATION_WEBHOOK`` - The URL for the webhook used for notifications. -``DISCORD_PING_LIST`` - A list of Discord Role IDs to ping whenever certain messages are sent. - ``DISCORD_MCM_WEBHOOK`` - The URL for the webhook used for Applications, Tickets, and Warnings. -``DISCORD_INVITE`` - The invite code to your Discord, for after a player applies on the web form. +``DISCORD_BOT_TOKEN`` - The token to use to run the Discord bot. This must be generated by you in the Discord developer area. + +``DISCORD_PING_LIST`` - A list of Discord Role IDs to ping whenever certain messages are sent. + +``DISCORD_BOT_PREFIX`` - The prefix to use for Discord bot commands. Set to ``!`` by default. + +``DISCORD_BOT_ROLES`` - A list of Discord Roles allowed to use the bot. If this list is empty, no one can use the bot! + +``DISCORD_SUPERUSER_ROLES`` - A list of Discord Roles allowed to use the superuser commands. + +``DISCORD_ERROR_USERS`` - A list of user IDs to send errors to. + +``DISCORD_MCM_CHANNEL`` - The ID for the channel used for Applications, Tickets, and Warnings. + +``DISCORD_NOTIFICATION_CHANNEL`` - The ID for the channel used for notifications. + +``INVITE_LINK`` - The invite link to your community. + +``INVITE_LABEL`` - The invite label for your community. ``DYNMAP_URL`` - The URL to your dynmap if you have one. Leave blank if you'd rather use a static background for web forms. @@ -51,14 +67,6 @@ Optional ``COREPROTECT_ACTIVITY_URL`` - The URL to your CoreProtect Activity Web UI, if it exists. -``DISCORD_BOT_TOKEN`` - The token to use to run the Discord bot. This must be generated by you in the Discord developer area. - -``DISCORD_BOT_PREFIX`` - The prefix to use for Discord bot commands. Set to ``!`` by default. - -``DISCORD_BOT_ROLES`` - A list of Discord Roles allowed to use the bot. If this list is empty, no one can use the bot! - -``DISCORD_BOT_NEW_MEMBER_ROLES`` - A list of Discord Roles to give new players when they register. - ``CAPTCHA_SECRET`` - Your secret key used for reCAPTCHA ``STATS_FILTER`` - A python list of partial strings used to filter out stats. e.g. ``['broken', 'dropped', 'picked_up']`` to filter out broken, dropped and picked up stats \ No newline at end of file diff --git a/external/views.py b/external/views.py index e3d58ee..5d40f82 100644 --- a/external/views.py +++ b/external/views.py @@ -1,5 +1,5 @@ from django.views.generic import View -from django.shortcuts import render, reverse, redirect +from django.shortcuts import render from django.conf import settings from django.utils.decorators import method_decorator from django.views.decorators.csrf import csrf_exempt @@ -8,12 +8,13 @@ import minecraft_manager.api.api as mcm_api import minecraft_manager.utils as mcm_utils import minecraft_manager.external.stats as mcm_stats from minecraft_manager.models import Player -import random, yaml, os, json, datetime, pytz +import random, yaml, os def config(): data = {} - data['discord_invite'] = getattr(settings, "DISCORD_INVITE", "#") + data['invite_link'] = getattr(settings, "INVITE_LINK", "#") + data['invite_label'] = getattr(settings, "INVITE_LABEL", "community") dynmap_url = getattr(settings, "DYNMAP_URL", "") data['dynmap_url'] = dynmap_url @@ -45,12 +46,16 @@ def config(): def rules(): path = os.path.join(settings.MINECRAFT_BASE_DIR, "plugins/MinecraftManager/config.yml") with open(path) as config_file: - config = yaml.load(config_file) - data = config['rules']['rules'] - if config['rules']['application']['validate']: - data.append("The answer to the final question is \"{}\"".format(config['rules']['application']['answer'])) + cfg = yaml.safe_load(config_file) + data = cfg['rules']['rules'] + if cfg['rules']['application']['validate']: + data.append("The answer to the final question is \"{}\"".format(cfg['rules']['application']['answer'])) - return data + return { + "rules": data, + "validate": cfg['rules']['application']['validate'], + "answer": cfg['rules']['application']['answer'] + } @method_decorator(csrf_exempt, name='dispatch') @@ -59,27 +64,31 @@ class Apply(View): def get(self, request): form = ApplicationForm() return render(request, 'minecraft_manager/external/apply.html', - {'form': form.as_p(), 'rules': rules(), 'valid': False, 'map': config(), - 'captcha': hasattr(settings, "CAPTCHA_SECRET")}) + {'form': form.as_p(), 'rules': rules()["rules"], 'valid': False, 'map': config(), + 'captcha': getattr(settings, "CAPTCHA_SITE", "")}) def post(self, request): form = ApplicationForm(request.POST) valid_username = mcm_utils.validate_username(form.data['username']) captcha = mcm_utils.Captcha(request.POST) valid = form.is_valid() - if valid and valid_username and captcha.success: + r = rules() + valid_answer = not r["validate"] or r["answer"] == form.data['read_rules'] + if valid and valid_username and valid_answer and captcha.success: app = form.save() msg = mcm_utils.build_application(app) - mcm_api.discord_mcm(message='New Application!', embeds=msg) + mcm_api.discord_mcm(message='New Application!', embed=msg) mcm_api.plugin("application", "{0} {1}".format(form.data['username'], app.id)) else: for error in captcha.errors: form.add_error(None, error) if not valid_username: form.add_error(None, "That username is not a premium Minecraft account") + if not valid_answer: + form.add_error(None, "Please read the rules again") return render(request, 'minecraft_manager/external/apply.html', - {'form': form.as_p(), 'rules': rules(), 'valid': valid and valid_username and captcha.success, 'map': config(), - 'captcha': hasattr(settings, "CAPTCHA_SECRET")}) + {'form': form.as_p(), 'rules': r["rules"], 'valid': valid and valid_username and valid_answer and captcha.success, 'map': config(), + 'captcha': getattr(settings, "CAPTCHA_SITE", "")}) @method_decorator(csrf_exempt, name='dispatch') @@ -89,7 +98,7 @@ class Ticket(View): form = TicketForm() return render(request, 'minecraft_manager/external/ticket.html', {'form': form.as_p(), 'valid': False, 'map': config(), - 'captcha': hasattr(settings, "CAPTCHA_SECRET")}) + 'captcha': getattr(settings, "CAPTCHA_SITE", "")}) def post(self, request): post = request.POST.copy() @@ -109,7 +118,7 @@ class Ticket(View): # Create the message to send to Discord link = "{}".format(mcm_utils.url_path(settings.MCM_BASE_LINK, 'dashboard/ticket', ticket.id)) msg = mcm_utils.build_ticket(ticket, link) - mcm_api.discord_mcm(message="New Ticket", embeds=msg, ping=True) + mcm_api.discord_mcm(message="New Ticket", embed=msg, ping=True) mcm_api.plugin("ticket", "{0} {1} {2}".format(username, ticket.id, link)) else: for error in captcha.errors: @@ -120,7 +129,7 @@ class Ticket(View): form.data['player'] = username return render(request, 'minecraft_manager/external/ticket.html', {'form': form.as_p(), 'valid': valid and captcha.success, 'map': config(), - 'captcha': hasattr(settings, "CAPTCHA_SECRET")}) + 'captcha': getattr(settings, "CAPTCHA_SITE", "")}) @method_decorator(csrf_exempt, name='dispatch') diff --git a/migrations/0014_auto_20190930_1103.py b/migrations/0014_auto_20190930_1103.py new file mode 100644 index 0000000..bfee727 --- /dev/null +++ b/migrations/0014_auto_20190930_1103.py @@ -0,0 +1,48 @@ +# Generated by Django 2.2.3 on 2019-09-30 16:03 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('minecraft_manager', '0013_warnings_to_notes'), + ] + + operations = [ + migrations.AlterModelOptions( + name='minecraftmanageruser', + options={'permissions': (('bots', 'Can use the bot control page'), ('chat', 'Can use chat page'))}, + ), + migrations.AlterModelOptions( + name='ticketnote', + options={'verbose_name': 'Ticket Note', 'verbose_name_plural': 'Ticket Notes'}, + ), + migrations.AlterField( + model_name='note', + name='player', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='note_player', to='minecraft_manager.Player'), + ), + migrations.AlterField( + model_name='note', + name='staff', + field=models.ForeignKey(blank=True, limit_choices_to=models.Q(is_active=True), null=True, on_delete=django.db.models.deletion.CASCADE, related_name='note_staff', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='player', + name='application', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='minecraft_manager.Application'), + ), + migrations.AlterField( + model_name='player', + name='auth_user', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='usersettings', + name='default_timezone', + field=models.CharField(choices=[('Africa/Abidjan', 'Africa/Abidjan'), ('Africa/Accra', 'Africa/Accra'), ('Africa/Addis_Ababa', 'Africa/Addis_Ababa'), ('Africa/Algiers', 'Africa/Algiers'), ('Africa/Asmara', 'Africa/Asmara'), ('Africa/Bamako', 'Africa/Bamako'), ('Africa/Bangui', 'Africa/Bangui'), ('Africa/Banjul', 'Africa/Banjul'), ('Africa/Bissau', 'Africa/Bissau'), ('Africa/Blantyre', 'Africa/Blantyre'), ('Africa/Brazzaville', 'Africa/Brazzaville'), ('Africa/Bujumbura', 'Africa/Bujumbura'), ('Africa/Cairo', 'Africa/Cairo'), ('Africa/Casablanca', 'Africa/Casablanca'), ('Africa/Ceuta', 'Africa/Ceuta'), ('Africa/Conakry', 'Africa/Conakry'), ('Africa/Dakar', 'Africa/Dakar'), ('Africa/Dar_es_Salaam', 'Africa/Dar_es_Salaam'), ('Africa/Djibouti', 'Africa/Djibouti'), ('Africa/Douala', 'Africa/Douala'), ('Africa/El_Aaiun', 'Africa/El_Aaiun'), ('Africa/Freetown', 'Africa/Freetown'), ('Africa/Gaborone', 'Africa/Gaborone'), ('Africa/Harare', 'Africa/Harare'), ('Africa/Johannesburg', 'Africa/Johannesburg'), ('Africa/Juba', 'Africa/Juba'), ('Africa/Kampala', 'Africa/Kampala'), ('Africa/Khartoum', 'Africa/Khartoum'), ('Africa/Kigali', 'Africa/Kigali'), ('Africa/Kinshasa', 'Africa/Kinshasa'), ('Africa/Lagos', 'Africa/Lagos'), ('Africa/Libreville', 'Africa/Libreville'), ('Africa/Lome', 'Africa/Lome'), ('Africa/Luanda', 'Africa/Luanda'), ('Africa/Lubumbashi', 'Africa/Lubumbashi'), ('Africa/Lusaka', 'Africa/Lusaka'), ('Africa/Malabo', 'Africa/Malabo'), ('Africa/Maputo', 'Africa/Maputo'), ('Africa/Maseru', 'Africa/Maseru'), ('Africa/Mbabane', 'Africa/Mbabane'), ('Africa/Mogadishu', 'Africa/Mogadishu'), ('Africa/Monrovia', 'Africa/Monrovia'), ('Africa/Nairobi', 'Africa/Nairobi'), ('Africa/Ndjamena', 'Africa/Ndjamena'), ('Africa/Niamey', 'Africa/Niamey'), ('Africa/Nouakchott', 'Africa/Nouakchott'), ('Africa/Ouagadougou', 'Africa/Ouagadougou'), ('Africa/Porto-Novo', 'Africa/Porto-Novo'), ('Africa/Sao_Tome', 'Africa/Sao_Tome'), ('Africa/Tripoli', 'Africa/Tripoli'), ('Africa/Tunis', 'Africa/Tunis'), ('Africa/Windhoek', 'Africa/Windhoek'), ('America/Adak', 'America/Adak'), ('America/Anchorage', 'America/Anchorage'), ('America/Anguilla', 'America/Anguilla'), ('America/Antigua', 'America/Antigua'), ('America/Araguaina', 'America/Araguaina'), ('America/Argentina/Buenos_Aires', 'America/Argentina/Buenos_Aires'), ('America/Argentina/Catamarca', 'America/Argentina/Catamarca'), ('America/Argentina/Cordoba', 'America/Argentina/Cordoba'), ('America/Argentina/Jujuy', 'America/Argentina/Jujuy'), ('America/Argentina/La_Rioja', 'America/Argentina/La_Rioja'), ('America/Argentina/Mendoza', 'America/Argentina/Mendoza'), ('America/Argentina/Rio_Gallegos', 'America/Argentina/Rio_Gallegos'), ('America/Argentina/Salta', 'America/Argentina/Salta'), ('America/Argentina/San_Juan', 'America/Argentina/San_Juan'), ('America/Argentina/San_Luis', 'America/Argentina/San_Luis'), ('America/Argentina/Tucuman', 'America/Argentina/Tucuman'), ('America/Argentina/Ushuaia', 'America/Argentina/Ushuaia'), ('America/Aruba', 'America/Aruba'), ('America/Asuncion', 'America/Asuncion'), ('America/Atikokan', 'America/Atikokan'), ('America/Bahia', 'America/Bahia'), ('America/Bahia_Banderas', 'America/Bahia_Banderas'), ('America/Barbados', 'America/Barbados'), ('America/Belem', 'America/Belem'), ('America/Belize', 'America/Belize'), ('America/Blanc-Sablon', 'America/Blanc-Sablon'), ('America/Boa_Vista', 'America/Boa_Vista'), ('America/Bogota', 'America/Bogota'), ('America/Boise', 'America/Boise'), ('America/Cambridge_Bay', 'America/Cambridge_Bay'), ('America/Campo_Grande', 'America/Campo_Grande'), ('America/Cancun', 'America/Cancun'), ('America/Caracas', 'America/Caracas'), ('America/Cayenne', 'America/Cayenne'), ('America/Cayman', 'America/Cayman'), ('America/Chicago', 'America/Chicago'), ('America/Chihuahua', 'America/Chihuahua'), ('America/Costa_Rica', 'America/Costa_Rica'), ('America/Creston', 'America/Creston'), ('America/Cuiaba', 'America/Cuiaba'), ('America/Curacao', 'America/Curacao'), ('America/Danmarkshavn', 'America/Danmarkshavn'), ('America/Dawson', 'America/Dawson'), ('America/Dawson_Creek', 'America/Dawson_Creek'), ('America/Denver', 'America/Denver'), ('America/Detroit', 'America/Detroit'), ('America/Dominica', 'America/Dominica'), ('America/Edmonton', 'America/Edmonton'), ('America/Eirunepe', 'America/Eirunepe'), ('America/El_Salvador', 'America/El_Salvador'), ('America/Fort_Nelson', 'America/Fort_Nelson'), ('America/Fortaleza', 'America/Fortaleza'), ('America/Glace_Bay', 'America/Glace_Bay'), ('America/Godthab', 'America/Godthab'), ('America/Goose_Bay', 'America/Goose_Bay'), ('America/Grand_Turk', 'America/Grand_Turk'), ('America/Grenada', 'America/Grenada'), ('America/Guadeloupe', 'America/Guadeloupe'), ('America/Guatemala', 'America/Guatemala'), ('America/Guayaquil', 'America/Guayaquil'), ('America/Guyana', 'America/Guyana'), ('America/Halifax', 'America/Halifax'), ('America/Havana', 'America/Havana'), ('America/Hermosillo', 'America/Hermosillo'), ('America/Indiana/Indianapolis', 'America/Indiana/Indianapolis'), ('America/Indiana/Knox', 'America/Indiana/Knox'), ('America/Indiana/Marengo', 'America/Indiana/Marengo'), ('America/Indiana/Petersburg', 'America/Indiana/Petersburg'), ('America/Indiana/Tell_City', 'America/Indiana/Tell_City'), ('America/Indiana/Vevay', 'America/Indiana/Vevay'), ('America/Indiana/Vincennes', 'America/Indiana/Vincennes'), ('America/Indiana/Winamac', 'America/Indiana/Winamac'), ('America/Inuvik', 'America/Inuvik'), ('America/Iqaluit', 'America/Iqaluit'), ('America/Jamaica', 'America/Jamaica'), ('America/Juneau', 'America/Juneau'), ('America/Kentucky/Louisville', 'America/Kentucky/Louisville'), ('America/Kentucky/Monticello', 'America/Kentucky/Monticello'), ('America/Kralendijk', 'America/Kralendijk'), ('America/La_Paz', 'America/La_Paz'), ('America/Lima', 'America/Lima'), ('America/Los_Angeles', 'America/Los_Angeles'), ('America/Lower_Princes', 'America/Lower_Princes'), ('America/Maceio', 'America/Maceio'), ('America/Managua', 'America/Managua'), ('America/Manaus', 'America/Manaus'), ('America/Marigot', 'America/Marigot'), ('America/Martinique', 'America/Martinique'), ('America/Matamoros', 'America/Matamoros'), ('America/Mazatlan', 'America/Mazatlan'), ('America/Menominee', 'America/Menominee'), ('America/Merida', 'America/Merida'), ('America/Metlakatla', 'America/Metlakatla'), ('America/Mexico_City', 'America/Mexico_City'), ('America/Miquelon', 'America/Miquelon'), ('America/Moncton', 'America/Moncton'), ('America/Monterrey', 'America/Monterrey'), ('America/Montevideo', 'America/Montevideo'), ('America/Montserrat', 'America/Montserrat'), ('America/Nassau', 'America/Nassau'), ('America/New_York', 'America/New_York'), ('America/Nipigon', 'America/Nipigon'), ('America/Nome', 'America/Nome'), ('America/Noronha', 'America/Noronha'), ('America/North_Dakota/Beulah', 'America/North_Dakota/Beulah'), ('America/North_Dakota/Center', 'America/North_Dakota/Center'), ('America/North_Dakota/New_Salem', 'America/North_Dakota/New_Salem'), ('America/Ojinaga', 'America/Ojinaga'), ('America/Panama', 'America/Panama'), ('America/Pangnirtung', 'America/Pangnirtung'), ('America/Paramaribo', 'America/Paramaribo'), ('America/Phoenix', 'America/Phoenix'), ('America/Port-au-Prince', 'America/Port-au-Prince'), ('America/Port_of_Spain', 'America/Port_of_Spain'), ('America/Porto_Velho', 'America/Porto_Velho'), ('America/Puerto_Rico', 'America/Puerto_Rico'), ('America/Punta_Arenas', 'America/Punta_Arenas'), ('America/Rainy_River', 'America/Rainy_River'), ('America/Rankin_Inlet', 'America/Rankin_Inlet'), ('America/Recife', 'America/Recife'), ('America/Regina', 'America/Regina'), ('America/Resolute', 'America/Resolute'), ('America/Rio_Branco', 'America/Rio_Branco'), ('America/Santarem', 'America/Santarem'), ('America/Santiago', 'America/Santiago'), ('America/Santo_Domingo', 'America/Santo_Domingo'), ('America/Sao_Paulo', 'America/Sao_Paulo'), ('America/Scoresbysund', 'America/Scoresbysund'), ('America/Sitka', 'America/Sitka'), ('America/St_Barthelemy', 'America/St_Barthelemy'), ('America/St_Johns', 'America/St_Johns'), ('America/St_Kitts', 'America/St_Kitts'), ('America/St_Lucia', 'America/St_Lucia'), ('America/St_Thomas', 'America/St_Thomas'), ('America/St_Vincent', 'America/St_Vincent'), ('America/Swift_Current', 'America/Swift_Current'), ('America/Tegucigalpa', 'America/Tegucigalpa'), ('America/Thule', 'America/Thule'), ('America/Thunder_Bay', 'America/Thunder_Bay'), ('America/Tijuana', 'America/Tijuana'), ('America/Toronto', 'America/Toronto'), ('America/Tortola', 'America/Tortola'), ('America/Vancouver', 'America/Vancouver'), ('America/Whitehorse', 'America/Whitehorse'), ('America/Winnipeg', 'America/Winnipeg'), ('America/Yakutat', 'America/Yakutat'), ('America/Yellowknife', 'America/Yellowknife'), ('Antarctica/Casey', 'Antarctica/Casey'), ('Antarctica/Davis', 'Antarctica/Davis'), ('Antarctica/DumontDUrville', 'Antarctica/DumontDUrville'), ('Antarctica/Macquarie', 'Antarctica/Macquarie'), ('Antarctica/Mawson', 'Antarctica/Mawson'), ('Antarctica/McMurdo', 'Antarctica/McMurdo'), ('Antarctica/Palmer', 'Antarctica/Palmer'), ('Antarctica/Rothera', 'Antarctica/Rothera'), ('Antarctica/Syowa', 'Antarctica/Syowa'), ('Antarctica/Troll', 'Antarctica/Troll'), ('Antarctica/Vostok', 'Antarctica/Vostok'), ('Arctic/Longyearbyen', 'Arctic/Longyearbyen'), ('Asia/Aden', 'Asia/Aden'), ('Asia/Almaty', 'Asia/Almaty'), ('Asia/Amman', 'Asia/Amman'), ('Asia/Anadyr', 'Asia/Anadyr'), ('Asia/Aqtau', 'Asia/Aqtau'), ('Asia/Aqtobe', 'Asia/Aqtobe'), ('Asia/Ashgabat', 'Asia/Ashgabat'), ('Asia/Atyrau', 'Asia/Atyrau'), ('Asia/Baghdad', 'Asia/Baghdad'), ('Asia/Bahrain', 'Asia/Bahrain'), ('Asia/Baku', 'Asia/Baku'), ('Asia/Bangkok', 'Asia/Bangkok'), ('Asia/Barnaul', 'Asia/Barnaul'), ('Asia/Beirut', 'Asia/Beirut'), ('Asia/Bishkek', 'Asia/Bishkek'), ('Asia/Brunei', 'Asia/Brunei'), ('Asia/Chita', 'Asia/Chita'), ('Asia/Choibalsan', 'Asia/Choibalsan'), ('Asia/Colombo', 'Asia/Colombo'), ('Asia/Damascus', 'Asia/Damascus'), ('Asia/Dhaka', 'Asia/Dhaka'), ('Asia/Dili', 'Asia/Dili'), ('Asia/Dubai', 'Asia/Dubai'), ('Asia/Dushanbe', 'Asia/Dushanbe'), ('Asia/Famagusta', 'Asia/Famagusta'), ('Asia/Gaza', 'Asia/Gaza'), ('Asia/Hebron', 'Asia/Hebron'), ('Asia/Ho_Chi_Minh', 'Asia/Ho_Chi_Minh'), ('Asia/Hong_Kong', 'Asia/Hong_Kong'), ('Asia/Hovd', 'Asia/Hovd'), ('Asia/Irkutsk', 'Asia/Irkutsk'), ('Asia/Jakarta', 'Asia/Jakarta'), ('Asia/Jayapura', 'Asia/Jayapura'), ('Asia/Jerusalem', 'Asia/Jerusalem'), ('Asia/Kabul', 'Asia/Kabul'), ('Asia/Kamchatka', 'Asia/Kamchatka'), ('Asia/Karachi', 'Asia/Karachi'), ('Asia/Kathmandu', 'Asia/Kathmandu'), ('Asia/Khandyga', 'Asia/Khandyga'), ('Asia/Kolkata', 'Asia/Kolkata'), ('Asia/Krasnoyarsk', 'Asia/Krasnoyarsk'), ('Asia/Kuala_Lumpur', 'Asia/Kuala_Lumpur'), ('Asia/Kuching', 'Asia/Kuching'), ('Asia/Kuwait', 'Asia/Kuwait'), ('Asia/Macau', 'Asia/Macau'), ('Asia/Magadan', 'Asia/Magadan'), ('Asia/Makassar', 'Asia/Makassar'), ('Asia/Manila', 'Asia/Manila'), ('Asia/Muscat', 'Asia/Muscat'), ('Asia/Nicosia', 'Asia/Nicosia'), ('Asia/Novokuznetsk', 'Asia/Novokuznetsk'), ('Asia/Novosibirsk', 'Asia/Novosibirsk'), ('Asia/Omsk', 'Asia/Omsk'), ('Asia/Oral', 'Asia/Oral'), ('Asia/Phnom_Penh', 'Asia/Phnom_Penh'), ('Asia/Pontianak', 'Asia/Pontianak'), ('Asia/Pyongyang', 'Asia/Pyongyang'), ('Asia/Qatar', 'Asia/Qatar'), ('Asia/Qostanay', 'Asia/Qostanay'), ('Asia/Qyzylorda', 'Asia/Qyzylorda'), ('Asia/Riyadh', 'Asia/Riyadh'), ('Asia/Sakhalin', 'Asia/Sakhalin'), ('Asia/Samarkand', 'Asia/Samarkand'), ('Asia/Seoul', 'Asia/Seoul'), ('Asia/Shanghai', 'Asia/Shanghai'), ('Asia/Singapore', 'Asia/Singapore'), ('Asia/Srednekolymsk', 'Asia/Srednekolymsk'), ('Asia/Taipei', 'Asia/Taipei'), ('Asia/Tashkent', 'Asia/Tashkent'), ('Asia/Tbilisi', 'Asia/Tbilisi'), ('Asia/Tehran', 'Asia/Tehran'), ('Asia/Thimphu', 'Asia/Thimphu'), ('Asia/Tokyo', 'Asia/Tokyo'), ('Asia/Tomsk', 'Asia/Tomsk'), ('Asia/Ulaanbaatar', 'Asia/Ulaanbaatar'), ('Asia/Urumqi', 'Asia/Urumqi'), ('Asia/Ust-Nera', 'Asia/Ust-Nera'), ('Asia/Vientiane', 'Asia/Vientiane'), ('Asia/Vladivostok', 'Asia/Vladivostok'), ('Asia/Yakutsk', 'Asia/Yakutsk'), ('Asia/Yangon', 'Asia/Yangon'), ('Asia/Yekaterinburg', 'Asia/Yekaterinburg'), ('Asia/Yerevan', 'Asia/Yerevan'), ('Atlantic/Azores', 'Atlantic/Azores'), ('Atlantic/Bermuda', 'Atlantic/Bermuda'), ('Atlantic/Canary', 'Atlantic/Canary'), ('Atlantic/Cape_Verde', 'Atlantic/Cape_Verde'), ('Atlantic/Faroe', 'Atlantic/Faroe'), ('Atlantic/Madeira', 'Atlantic/Madeira'), ('Atlantic/Reykjavik', 'Atlantic/Reykjavik'), ('Atlantic/South_Georgia', 'Atlantic/South_Georgia'), ('Atlantic/St_Helena', 'Atlantic/St_Helena'), ('Atlantic/Stanley', 'Atlantic/Stanley'), ('Australia/Adelaide', 'Australia/Adelaide'), ('Australia/Brisbane', 'Australia/Brisbane'), ('Australia/Broken_Hill', 'Australia/Broken_Hill'), ('Australia/Currie', 'Australia/Currie'), ('Australia/Darwin', 'Australia/Darwin'), ('Australia/Eucla', 'Australia/Eucla'), ('Australia/Hobart', 'Australia/Hobart'), ('Australia/Lindeman', 'Australia/Lindeman'), ('Australia/Lord_Howe', 'Australia/Lord_Howe'), ('Australia/Melbourne', 'Australia/Melbourne'), ('Australia/Perth', 'Australia/Perth'), ('Australia/Sydney', 'Australia/Sydney'), ('Canada/Atlantic', 'Canada/Atlantic'), ('Canada/Central', 'Canada/Central'), ('Canada/Eastern', 'Canada/Eastern'), ('Canada/Mountain', 'Canada/Mountain'), ('Canada/Newfoundland', 'Canada/Newfoundland'), ('Canada/Pacific', 'Canada/Pacific'), ('Europe/Amsterdam', 'Europe/Amsterdam'), ('Europe/Andorra', 'Europe/Andorra'), ('Europe/Astrakhan', 'Europe/Astrakhan'), ('Europe/Athens', 'Europe/Athens'), ('Europe/Belgrade', 'Europe/Belgrade'), ('Europe/Berlin', 'Europe/Berlin'), ('Europe/Bratislava', 'Europe/Bratislava'), ('Europe/Brussels', 'Europe/Brussels'), ('Europe/Bucharest', 'Europe/Bucharest'), ('Europe/Budapest', 'Europe/Budapest'), ('Europe/Busingen', 'Europe/Busingen'), ('Europe/Chisinau', 'Europe/Chisinau'), ('Europe/Copenhagen', 'Europe/Copenhagen'), ('Europe/Dublin', 'Europe/Dublin'), ('Europe/Gibraltar', 'Europe/Gibraltar'), ('Europe/Guernsey', 'Europe/Guernsey'), ('Europe/Helsinki', 'Europe/Helsinki'), ('Europe/Isle_of_Man', 'Europe/Isle_of_Man'), ('Europe/Istanbul', 'Europe/Istanbul'), ('Europe/Jersey', 'Europe/Jersey'), ('Europe/Kaliningrad', 'Europe/Kaliningrad'), ('Europe/Kiev', 'Europe/Kiev'), ('Europe/Kirov', 'Europe/Kirov'), ('Europe/Lisbon', 'Europe/Lisbon'), ('Europe/Ljubljana', 'Europe/Ljubljana'), ('Europe/London', 'Europe/London'), ('Europe/Luxembourg', 'Europe/Luxembourg'), ('Europe/Madrid', 'Europe/Madrid'), ('Europe/Malta', 'Europe/Malta'), ('Europe/Mariehamn', 'Europe/Mariehamn'), ('Europe/Minsk', 'Europe/Minsk'), ('Europe/Monaco', 'Europe/Monaco'), ('Europe/Moscow', 'Europe/Moscow'), ('Europe/Oslo', 'Europe/Oslo'), ('Europe/Paris', 'Europe/Paris'), ('Europe/Podgorica', 'Europe/Podgorica'), ('Europe/Prague', 'Europe/Prague'), ('Europe/Riga', 'Europe/Riga'), ('Europe/Rome', 'Europe/Rome'), ('Europe/Samara', 'Europe/Samara'), ('Europe/San_Marino', 'Europe/San_Marino'), ('Europe/Sarajevo', 'Europe/Sarajevo'), ('Europe/Saratov', 'Europe/Saratov'), ('Europe/Simferopol', 'Europe/Simferopol'), ('Europe/Skopje', 'Europe/Skopje'), ('Europe/Sofia', 'Europe/Sofia'), ('Europe/Stockholm', 'Europe/Stockholm'), ('Europe/Tallinn', 'Europe/Tallinn'), ('Europe/Tirane', 'Europe/Tirane'), ('Europe/Ulyanovsk', 'Europe/Ulyanovsk'), ('Europe/Uzhgorod', 'Europe/Uzhgorod'), ('Europe/Vaduz', 'Europe/Vaduz'), ('Europe/Vatican', 'Europe/Vatican'), ('Europe/Vienna', 'Europe/Vienna'), ('Europe/Vilnius', 'Europe/Vilnius'), ('Europe/Volgograd', 'Europe/Volgograd'), ('Europe/Warsaw', 'Europe/Warsaw'), ('Europe/Zagreb', 'Europe/Zagreb'), ('Europe/Zaporozhye', 'Europe/Zaporozhye'), ('Europe/Zurich', 'Europe/Zurich'), ('GMT', 'GMT'), ('Indian/Antananarivo', 'Indian/Antananarivo'), ('Indian/Chagos', 'Indian/Chagos'), ('Indian/Christmas', 'Indian/Christmas'), ('Indian/Cocos', 'Indian/Cocos'), ('Indian/Comoro', 'Indian/Comoro'), ('Indian/Kerguelen', 'Indian/Kerguelen'), ('Indian/Mahe', 'Indian/Mahe'), ('Indian/Maldives', 'Indian/Maldives'), ('Indian/Mauritius', 'Indian/Mauritius'), ('Indian/Mayotte', 'Indian/Mayotte'), ('Indian/Reunion', 'Indian/Reunion'), ('Pacific/Apia', 'Pacific/Apia'), ('Pacific/Auckland', 'Pacific/Auckland'), ('Pacific/Bougainville', 'Pacific/Bougainville'), ('Pacific/Chatham', 'Pacific/Chatham'), ('Pacific/Chuuk', 'Pacific/Chuuk'), ('Pacific/Easter', 'Pacific/Easter'), ('Pacific/Efate', 'Pacific/Efate'), ('Pacific/Enderbury', 'Pacific/Enderbury'), ('Pacific/Fakaofo', 'Pacific/Fakaofo'), ('Pacific/Fiji', 'Pacific/Fiji'), ('Pacific/Funafuti', 'Pacific/Funafuti'), ('Pacific/Galapagos', 'Pacific/Galapagos'), ('Pacific/Gambier', 'Pacific/Gambier'), ('Pacific/Guadalcanal', 'Pacific/Guadalcanal'), ('Pacific/Guam', 'Pacific/Guam'), ('Pacific/Honolulu', 'Pacific/Honolulu'), ('Pacific/Kiritimati', 'Pacific/Kiritimati'), ('Pacific/Kosrae', 'Pacific/Kosrae'), ('Pacific/Kwajalein', 'Pacific/Kwajalein'), ('Pacific/Majuro', 'Pacific/Majuro'), ('Pacific/Marquesas', 'Pacific/Marquesas'), ('Pacific/Midway', 'Pacific/Midway'), ('Pacific/Nauru', 'Pacific/Nauru'), ('Pacific/Niue', 'Pacific/Niue'), ('Pacific/Norfolk', 'Pacific/Norfolk'), ('Pacific/Noumea', 'Pacific/Noumea'), ('Pacific/Pago_Pago', 'Pacific/Pago_Pago'), ('Pacific/Palau', 'Pacific/Palau'), ('Pacific/Pitcairn', 'Pacific/Pitcairn'), ('Pacific/Pohnpei', 'Pacific/Pohnpei'), ('Pacific/Port_Moresby', 'Pacific/Port_Moresby'), ('Pacific/Rarotonga', 'Pacific/Rarotonga'), ('Pacific/Saipan', 'Pacific/Saipan'), ('Pacific/Tahiti', 'Pacific/Tahiti'), ('Pacific/Tarawa', 'Pacific/Tarawa'), ('Pacific/Tongatapu', 'Pacific/Tongatapu'), ('Pacific/Wake', 'Pacific/Wake'), ('Pacific/Wallis', 'Pacific/Wallis'), ('US/Alaska', 'US/Alaska'), ('US/Arizona', 'US/Arizona'), ('US/Central', 'US/Central'), ('US/Eastern', 'US/Eastern'), ('US/Hawaii', 'US/Hawaii'), ('US/Mountain', 'US/Mountain'), ('US/Pacific', 'US/Pacific'), ('UTC', 'UTC')], default='UTC', max_length=20, verbose_name='Timezone'), + ), + ] diff --git a/models.py b/models.py index 8a9e757..095bf16 100644 --- a/models.py +++ b/models.py @@ -13,8 +13,8 @@ class MinecraftManagerUser(User): class Meta: proxy = True permissions = ( - ('minecraft_manager_bots', 'Can use the bot control page'), - ('minecraft_manager_chat', 'Can use chat page'), + ('bots', 'Can use the bot control page'), + ('chat', 'Can use chat page'), ) diff --git a/overview.py b/overview.py index e6eb602..2b5f56c 100644 --- a/overview.py +++ b/overview.py @@ -46,11 +46,11 @@ def overview_data(): # Percentage data['percentage'] = { 'accepted': round((data['total']['application']['accepted'] / - data['total']['application']['all'] if data['total']['application']['all'] != 0 else 1) * 100, 2), + data['total']['application']['all'] if data['total']['application']['all'] != 0 else 1) * 100, 2), 'banned': round((data['total']['player']['banned'] / - data['total']['player']['all'] if data['total']['player']['all'] != 0 else 1) * 100, 2), + data['total']['player']['all'] if data['total']['player']['all'] != 0 else 1) * 100, 2), 'applied': round((data['total']['application']['all'] / - data['total']['player']['all'] if data['total']['player']['all'] != 0 else 1) * 100, 2) + data['total']['player']['all'] if data['total']['player']['all'] != 0 else 1) * 100, 2) } # Unique logins diff --git a/static/minecraft_manager/css/external.css b/static/minecraft_manager/css/external.css index 0b2adc0..8f3436b 100644 --- a/static/minecraft_manager/css/external.css +++ b/static/minecraft_manager/css/external.css @@ -67,4 +67,9 @@ .rule { margin-bottom: .5em; +} + +.errorlist { + color: #D8000C; + background-color: #FFD2D2; } \ No newline at end of file diff --git a/templates/minecraft_manager/bots.html b/templates/minecraft_manager/bots.html index 6460c81..d0ffd81 100644 --- a/templates/minecraft_manager/bots.html +++ b/templates/minecraft_manager/bots.html @@ -1,16 +1,14 @@ {% extends "minecraft_manager/dashboard.html" %} {% load csrf_html %} -{% load getattribute %} {% block title %}Bots{% endblock %} {% block section %}
- - {% for bot in form.bots %} -

{{ bot.name }}: {% if form|getattribute:bot.name is True %}Started{% else %}Stopped{% endif %}

+ {% for bot in bots %} +

{{ bot.name }}: {{ bot.display }}

{% autoescape off %}{% get_csrf_html request %}{% endautoescape %} - - - + + +

{% endfor %} diff --git a/templates/minecraft_manager/coreprotect.html b/templates/minecraft_manager/coreprotect.html index 3d2478d..6848e59 100644 --- a/templates/minecraft_manager/coreprotect.html +++ b/templates/minecraft_manager/coreprotect.html @@ -1,12 +1,13 @@ -{% extends "minecraft_manager/dashboard.html" %} -{% load template_settings %} -{% block title %}CoreProtect GUI{% endblock %} -{% block section %} - Fullscreen - - - -{% endblock section %} \ No newline at end of file +{% if show_gui and show_activity %} + +{% elif show_gui %} +
  • CoreProtect GUI
  • +{% elif show_activity %} +
  • Activity Monitor
  • +{% endif %} \ No newline at end of file diff --git a/templates/minecraft_manager/dashboard.html b/templates/minecraft_manager/dashboard.html index 94fae09..d63e4d9 100644 --- a/templates/minecraft_manager/dashboard.html +++ b/templates/minecraft_manager/dashboard.html @@ -3,6 +3,7 @@ {% load csrf_html %} {% load sidebar %} {% load template_settings %} +{% load coreprotect %} {% block bootstrap %} {% if user.usersettings.default_theme == 'DA' %} @@ -23,21 +24,7 @@