From 773efe060662ecf7dda3e57eca64b608a2449ab2 Mon Sep 17 00:00:00 2001 From: Etzelia Date: Tue, 27 Aug 2019 22:44:20 +0200 Subject: [PATCH] Updates to API and Discord (#28) --- api/api.py | 21 ++-- api/bot.py | 212 --------------------------------- api/views.py | 6 +- assets/bots/Discord-MCM.bot.py | 7 +- assets/coreprotect/blocks.txt | 9 -- bot/__init__.py | 0 bot/commands.py | 205 +++++++++++++++++++++++++++++++ bot/discord.py | 80 +++++++++++++ bot/utils.py | 27 +++++ templatetags/sidebar.py | 4 +- urls.py | 6 +- 11 files changed, 338 insertions(+), 239 deletions(-) delete mode 100644 api/bot.py delete mode 100644 assets/coreprotect/blocks.txt create mode 100644 bot/__init__.py create mode 100644 bot/commands.py create mode 100644 bot/discord.py create mode 100644 bot/utils.py diff --git a/api/api.py b/api/api.py index ab98156..fc04bfb 100644 --- a/api/api.py +++ b/api/api.py @@ -22,14 +22,19 @@ 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): 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..675452e 100644 --- a/api/views.py +++ b/api/views.py @@ -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 diff --git a/assets/bots/Discord-MCM.bot.py b/assets/bots/Discord-MCM.bot.py index ef882c9..092a99e 100644 --- a/assets/bots/Discord-MCM.bot.py +++ b/assets/bots/Discord-MCM.bot.py @@ -1,4 +1,6 @@ -import os, sys, django +import os +import sys +import django sep = os.sep path = os.path.dirname(os.path.abspath(__file__)) @@ -11,8 +13,9 @@ 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/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/bot/__init__.py b/bot/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bot/commands.py b/bot/commands.py new file mode 100644 index 0000000..0a3140c --- /dev/null +++ b/bot/commands.py @@ -0,0 +1,205 @@ +import discord +from discord.ext import commands +from django.contrib.auth.models import User + +from minecraft_manager.api import api +from minecraft_manager.bot.utils import get_application, build_info +from minecraft_manager.models import Application, Player + + +class Commands(commands.Cog): + + def __init__(self, bot): + self.bot = bot + + 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.") + + @app.command("info") + 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 + + 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_info(application)) + + @app.command("accept") + 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?") + + @app.command("deny") + 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?") + + @app.command("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_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: + await self.bot.discord_message(ctx.message.channel, "**No applications matched, however there is a player match**") + info = 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." + 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..970ccaf --- /dev/null +++ b/bot/discord.py @@ -0,0 +1,80 @@ +import asyncio +import logging +import sys +import traceback + +import discord +from discord.ext import commands +from django.conf import settings +from django.db import close_old_connections + +from minecraft_manager.bot.commands import Commands + +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__)) + + 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 before_invoke(self, coro): + # FIX STALE DB CONNECTIONS + close_old_connections() + + 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..9a87c2b --- /dev/null +++ b/bot/utils.py @@ -0,0 +1,27 @@ +import discord +from minecraft_manager.models import Application, Player + + +def build_info(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 + + +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/templatetags/sidebar.py b/templatetags/sidebar.py index ca9fddb..77945c0 100644 --- a/templatetags/sidebar.py +++ b/templatetags/sidebar.py @@ -31,8 +31,8 @@ def get_sidebar(current_app, request): ret += '
  •   Report
  • '.format('class="active"' if current_app == 'report' else '', reverse('report')) show_chat = True if getattr(settings, 'GLOBAL_LOG', None) is not None else False - if show_chat and request.user.has_perm('auth.chat'): + if show_chat and request.user.has_perm('minecraft_manager.chat'): ret += '
  •   Chat
  • '.format('class="active"' if current_app == 'chat' else '', reverse('chat')) - if request.user.has_perm('auth.bots'): + if request.user.has_perm('minecraft_manager.bots'): ret += '
  •   Bots
  • '.format('class="active"' if current_app == 'bots' else '', reverse('bots')) return ret diff --git a/urls.py b/urls.py index fe9d640..5be6156 100644 --- a/urls.py +++ b/urls.py @@ -1,6 +1,6 @@ from django.conf.urls import url from django.views.generic import RedirectView -from django.contrib.auth.decorators import login_required +from django.contrib.auth.decorators import login_required, permission_required import minecraft_manager.views as mcm urlpatterns = [ @@ -33,9 +33,9 @@ urlpatterns = [ #Report url(r'^report/$', login_required(mcm.Report.as_view()), name="report"), #Chat - url(r'^dashboard/chat/$', login_required(mcm.Chat.as_view()), name="chat"), + url(r'^dashboard/chat/$', permission_required('minecraft_manager.chat')(mcm.Chat.as_view()), name="chat"), #Bots - url(r'^dashboard/bots/$', login_required(mcm.Bots.as_view()), name="bots"), + url(r'^dashboard/bots/$', permission_required('minecraft_manager.bots')(mcm.Bots.as_view()), name="bots"), ]