Updates to API and Discord (#28)

reminder
Etzelia 2019-08-27 22:44:20 +02:00 committed by Gitea
parent da66751986
commit 773efe0606
11 changed files with 338 additions and 239 deletions

View File

@ -22,6 +22,7 @@ PLUGIN_DEMOTE = 'demote'
def plugin(key, command): def plugin(key, command):
try:
host = '127.0.0.1' host = '127.0.0.1'
port = getattr(settings, 'PLUGIN_PORT', None) port = getattr(settings, 'PLUGIN_PORT', None)
full_command = "{0} {1}".format(key, command) full_command = "{0} {1}".format(key, command)
@ -30,6 +31,10 @@ def plugin(key, command):
sock.connect((host, port)) sock.connect((host, port))
sock.sendall(full_command.encode('utf-8')) sock.sendall(full_command.encode('utf-8'))
sock.close() sock.close()
return True
except:
pass
return False
def discord_mcm(message='', embeds=None, ping=False): def discord_mcm(message='', embeds=None, ping=False):

View File

@ -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 <username>".format(self.prefix), value="Search for applications by partial or exact username.")
embed.add_field(name="{}[app ]info <app ID>".format(self.prefix), value="Get detailed information about a specific application.")
embed.add_field(name="{}[app ]accept|deny <app ID>".format(self.prefix), value="Take action on an application.")
embed.add_field(name="{}demote <username>".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)

View File

@ -138,11 +138,11 @@ class WebAPI(View):
def access_level(self, user): def access_level(self, user):
access = {'cpp': False, 'cpf': False, 'cpa': False} 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 access['cpp'] = True
if user.has_perm('auth.coreprotect_full'): if user.has_perm('minecraft_manager.coreprotect_full'):
access['cpf'] = True access['cpf'] = True
if user.has_perm('auth.coreprotect_activity'): if user.has_perm('minecraft_manager.coreprotect_activity'):
access['cpa'] = True access['cpa'] = True
return access return access

View File

@ -1,4 +1,6 @@
import os, sys, django import os
import sys
import django
sep = os.sep sep = os.sep
path = os.path.dirname(os.path.abspath(__file__)) path = os.path.dirname(os.path.abspath(__file__))
@ -11,8 +13,9 @@ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "{}.settings".format(project))
django.setup() django.setup()
from django.conf import settings 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) token = getattr(settings, 'DISCORD_BOT_TOKEN', None)
bot = Discord(token) bot = Discord(token)
bot.run_bot() bot.run_bot()

View File

@ -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 <coreprotect database> -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

0
bot/__init__.py 100644
View File

205
bot/commands.py 100644
View File

@ -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 <username>".format(self.bot.prefix),
value="Search for applications by partial or exact username.")
embed.add_field(name="{}app info <app ID>".format(self.bot.prefix),
value="Get detailed information about a specific application.")
embed.add_field(name="{}app accept|deny <app ID>".format(self.bot.prefix), value="Take action on an application.")
embed.add_field(name="{}demote <username>".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))

80
bot/discord.py 100644
View File

@ -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...")

27
bot/utils.py 100644
View File

@ -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

View File

@ -31,8 +31,8 @@ def get_sidebar(current_app, request):
ret += '<li {}><a href="{}"><span class="glyphicon glyphicon-wrench"></span>&nbsp;&nbsp;Report</a></li>'.format('class="active"' if current_app == 'report' else '', reverse('report')) ret += '<li {}><a href="{}"><span class="glyphicon glyphicon-wrench"></span>&nbsp;&nbsp;Report</a></li>'.format('class="active"' if current_app == 'report' else '', reverse('report'))
show_chat = True if getattr(settings, 'GLOBAL_LOG', None) is not None else False 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 += '<li {}><a href="{}"><span class="glyphicon glyphicon-comment"></span>&nbsp;&nbsp;Chat</a></li>'.format('class="active"' if current_app == 'chat' else '', reverse('chat')) ret += '<li {}><a href="{}"><span class="glyphicon glyphicon-comment"></span>&nbsp;&nbsp;Chat</a></li>'.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 += '<li {}><a href="{}"><span class="glyphicon glyphicon-flash"></span>&nbsp;&nbsp;Bots</a></li>'.format('class="active"' if current_app == 'bots' else '', reverse('bots')) ret += '<li {}><a href="{}"><span class="glyphicon glyphicon-flash"></span>&nbsp;&nbsp;Bots</a></li>'.format('class="active"' if current_app == 'bots' else '', reverse('bots'))
return ret return ret

View File

@ -1,6 +1,6 @@
from django.conf.urls import url from django.conf.urls import url
from django.views.generic import RedirectView 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 import minecraft_manager.views as mcm
urlpatterns = [ urlpatterns = [
@ -33,9 +33,9 @@ urlpatterns = [
#Report #Report
url(r'^report/$', login_required(mcm.Report.as_view()), name="report"), url(r'^report/$', login_required(mcm.Report.as_view()), name="report"),
#Chat #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 #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"),
] ]