Internalize Discord Bot (#31)

reminder
Etzelia 2019-08-30 20:03:12 +02:00 committed by Gitea
parent 8073765a57
commit c813535489
11 changed files with 184 additions and 169 deletions

View File

@ -1,7 +1,8 @@
import socket, requests, logging, os, datetime, pytz, mcstatus, random, string
import socket, logging, os, datetime, pytz, mcstatus, random, string
from minecraft_manager.models import Alert
from django.contrib.auth.models import User
from django.conf import settings
from minecraft_manager.bot.discord import send as discord_send, DestType
logger = logging.getLogger(__name__)
@ -37,36 +38,24 @@ def plugin(key, command):
return False
def discord_mcm(message='', embeds=None, ping=False):
discord_mcm_webhook = getattr(settings, 'DISCORD_MCM_WEBHOOK', None)
if discord_mcm_webhook:
def discord_mcm(message='', embed=None, ping=False):
channel_id = getattr(settings, 'DISCORD_MCM_CHANNEL', None)
if channel_id:
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 None
discord_send(DestType.CHANNEL, channel_id, message, embed)
def discord_notification(message='', embeds=None, ping=False):
discord_notification_webhook = getattr(settings, 'DISCORD_NOTIFICATION_WEBHOOK', None)
if discord_notification_webhook:
def discord_notification(message='', embed=None, ping=False):
channel_id = getattr(settings, 'DISCORD_NOTIFICATION_CHANNEL', None)
if channel_id:
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 None
discord_send(DestType.CHANNEL, channel_id, message, embed)

View File

@ -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'])
@ -298,7 +298,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 +311,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."

View File

@ -1,21 +0,0 @@
import os
import sys
import django
sep = os.sep
path = os.path.dirname(os.path.abspath(__file__))
path = path.split(sep)[:-3]
project = path[-1]
path = sep.join(path)
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.bot.discord import Discord
token = getattr(settings, 'DISCORD_BOT_TOKEN', None)
bot = Discord(token)
bot.run_bot()

View File

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

View File

@ -0,0 +1,38 @@
from django.conf import settings
import subprocess
class Bot:
plugin_port = getattr(settings, 'PLUGIN_PORT', None)
bot_dir = getattr(settings, 'BOT_DIR', "")
if not bot_dir.endswith("/"):
bot_dir += "/"
def __init__(self, name, asset, executable=None, start=None, stop=None, restart=None, status=None, display=None):
self.name = name
self.asset = asset
self.executable = executable
self.start = start if start else self._start
self.stop = stop if stop else self._stop
self.restart = restart if restart else self._restart
self.status = status if status else self._status
self.display = display if display else self._display
def _start(self):
screen = 'screen -S {0}_{1} -d -m {2} {3}{1}.bot.py'
subprocess.run(screen.format(self.plugin_port, self.name, self.executable, self.bot_dir),
shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
def _stop(self):
subprocess.run('screen -X -S "{0}_{1}" quit'.format(self.plugin_port, self.name), shell=True)
def _restart(self):
self.stop()
self.start()
def _status(self):
screens = subprocess.getoutput("screen -ls")
return True if "{0}_{1}".format(self.plugin_port, self.name) in screens else False
def _display(self):
return "Started" if self.status() else "Stopped"

View File

@ -1,21 +1,38 @@
import asyncio
import logging
import sys
import traceback
import threading
from enum import Enum
import discord
from discord.ext import commands
from django.conf import settings
from minecraft_manager.bot.commands import Commands
logger = logging.getLogger(__name__)
discord_loop = None
discord_bot = None
description = '''
A Discord bot connected to an MCM instance.
'''
class DiscordStatus(Enum):
STOPPED = 0
STARTING = 1
STARTED = 2
STOPPING = 3
def __str__(self):
return self.name.title()
def is_running(self):
return self != DiscordStatus.STOPPED
discord_status = DiscordStatus.STOPPED
class Discord(commands.Bot):
discord_game = 'MCM'
prefix = getattr(settings, 'DISCORD_BOT_PREFIX', '!')
@ -23,12 +40,14 @@ class Discord(commands.Bot):
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))
def __init__(self, token, loop):
super().__init__(command_prefix=self.prefix, description=description, case_insensitive=True, help_command=None, activity=discord.Game(name=self.discord_game), loop=loop)
self.token = token
self.load_extension("minecraft_manager.bot.commands")
async def on_ready(self):
global discord_status
discord_status = DiscordStatus.STARTED
print('Logged in as')
print(self.user.name)
print(self.user.id)
@ -38,6 +57,10 @@ class Discord(commands.Bot):
print('------')
print('Logged in as {0} ({1}) with discord.py v{2}'.format(self.user.name, self.user.id, discord.__version__))
async def on_disconnect(self):
global discord_status
discord_status = DiscordStatus.STOPPED
async def discord_message(self, dest, message):
if isinstance(message, discord.Embed):
for idx, field in enumerate(message.fields):
@ -61,15 +84,77 @@ class Discord(commands.Bot):
await self.discord_message(user, '```python\n{}```'.format(error))
def run_bot(self):
loop = asyncio.get_event_loop()
global discord_loop
try:
loop.run_until_complete(self.start(self.token))
discord_loop.run_until_complete(self.start(self.token))
except KeyboardInterrupt:
logger.info("Bot received keyboard interrupt")
except Exception as e:
print(e)
logger.info('Bot encountered the following unhandled exception %s', e)
finally:
loop.run_until_complete(self.logout())
logger.info("Bot shutting down...")
def start():
global discord_loop, discord_bot, discord_status
if discord_status != DiscordStatus.STOPPED:
return
token = getattr(settings, 'DISCORD_BOT_TOKEN', None)
discord_loop = asyncio.new_event_loop()
discord_bot = Discord(token, discord_loop)
thread = threading.Thread(target=discord_bot.run_bot)
thread.start()
discord_status = DiscordStatus.STARTING
def stop():
global discord_loop, discord_bot, discord_status
if discord_status == DiscordStatus.STARTED:
discord_loop.create_task(discord_bot.close())
discord_status = DiscordStatus.STOPPING
discord_loop = None
discord_bot = None
def restart():
def _restart():
stop()
while discord_status.is_running():
pass
start()
if discord_status != DiscordStatus.STARTED:
return
thread = threading.Thread(target=_restart)
thread.start()
def status():
return discord_status.is_running()
def display():
return str(discord_status)
class DestType(Enum):
CHANNEL = 1
USER = 2
def send(dest_type: DestType, dest_id: int, message: str = "", embed: discord.Embed = None):
async def _send():
if dest_type == DestType.CHANNEL:
dest = discord_bot.get_channel(dest_id)
elif dest_type == DestType.USER:
dest = discord_bot.get_user(dest_id)
else:
return
if message is not None:
await dest.send(message)
if embed is not None:
for idx, field in enumerate(embed.fields):
if not field.value:
embed.set_field_at(idx, name=field.name, value="N/A")
await dest.send(embed=embed)
global discord_loop, discord_bot
if discord_loop:
discord_loop.create_task(_send())

View File

@ -23,11 +23,21 @@ Optional
``BOT_DIR`` - The path to your bot directory.
``DISCORD_NOTIFICATION_WEBHOOK`` - The URL for the webhook used for notifications.
``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_MCM_WEBHOOK`` - The URL for the webhook used for Applications, Tickets, and Warnings.
``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.
``DISCORD_INVITE`` - The invite code to your Discord, for after a player applies on the web form.
@ -51,14 +61,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

4
external/views.py vendored
View File

@ -70,7 +70,7 @@ class Apply(View):
if valid and valid_username 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:
@ -109,7 +109,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:

View File

@ -1,16 +1,14 @@
{% extends "minecraft_manager/dashboard.html" %}
{% load csrf_html %}
{% load getattribute %}
{% block title %}Bots{% endblock %}
{% block section %}
<div id="content">
<!-- {{ form.status }} -->
{% for bot in form.bots %}
<p>{{ bot.name }}: <span class="label {% if form|getattribute:bot.name is True %}label-success{% else %}label-danger{% endif %}">{% if form|getattribute:bot.name is True %}Started{% else %}Stopped{% endif %}</span></p>
{% for bot in bots %}
<p>{{ bot.name }}: <span class="label {% if bot.status is True %}label-success{% else %}label-danger{% endif %}">{{ bot.display }}</span></p>
<form action="" method="post">{% autoescape off %}{% get_csrf_html request %}{% endautoescape %}
<button class="btn btn-primary{% if form|getattribute:bot.name is True %} disabled{% endif %}" type="submit" name="{{ bot.name }}" value="start" {% if form|getattribute:bot.name is True %} disabled="disabled"{% endif %}>Start</button>
<button class="btn btn-primary{% if form|getattribute:bot.name is False %} disabled{% endif %}" type="submit" name="{{ bot.name }}" value="stop" {% if form|getattribute:bot.name is False %}disabled="disabled"{% endif %}>Stop</button>
<button class="btn btn-primary{% if form|getattribute:bot.name is False %} disabled{% endif %}" type="submit" name="{{ bot.name }}" value="restart" {% if form|getattribute:bot.name is False %}disabled="disabled"{% endif %}>Restart</button>
<button class="btn btn-primary{% if bot.status is True %} disabled{% endif %}" type="submit" name="{{ bot.name }}" value="start" {% if bot.status is True %} disabled="disabled"{% endif %}>Start</button>
<button class="btn btn-primary{% if bot.status is False %} disabled{% endif %}" type="submit" name="{{ bot.name }}" value="stop" {% if bot.status is False %}disabled="disabled"{% endif %}>Stop</button>
<button class="btn btn-primary{% if bot.status is False %} disabled{% endif %}" type="submit" name="{{ bot.name }}" value="restart" {% if bot.status is False %}disabled="disabled"{% endif %}>Restart</button>
</form>
<br/>
{% endfor %}

View File

@ -38,7 +38,7 @@ def build_application(application):
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.to_dict()]
return embed
def build_ticket(ticket, link):
@ -53,7 +53,7 @@ def build_ticket(ticket, link):
embed.add_field(name="Location", value=ticket.location)
embed.add_field(name="Message", value=ticket.message)
embed.add_field(name="Link", value=link)
return [embed.to_dict()]
return embed
def build_warning(warning, link):
@ -66,7 +66,7 @@ def build_warning(warning, link):
embed.add_field(name="Importance", value=warning.importance_display)
embed.add_field(name="Message", value=warning.message)
embed.add_field(name="Link", value=link)
return [embed.to_dict()]
return embed
def validate_username(username):

View File

@ -17,8 +17,9 @@ from minecraft_manager.forms import TicketNoteForm, NoteForm
from minecraft_manager.overview import overview_data
from minecraft_manager.utils import resolve_player
import minecraft_manager.api.api as API
import subprocess
from minecraft_manager.bot import Bot
from minecraft_manager.bot.discord import start as discord_start, stop as discord_stop, restart as discord_restart, \
status as discord_status, display as discord_display
class Overview(View):
@ -452,11 +453,6 @@ class Chat(View):
class Bots(View):
def assets(self):
path = os.path.abspath(os.path.dirname(__file__))
bot_dir = os.path.join(path, 'assets/bots')
return bot_dir
def get_bots(self):
bot_dir = getattr(settings, 'BOT_DIR', None)
bots = []
@ -466,41 +462,24 @@ class Bots(View):
ve = file.replace('.bot.py', '')
py = os.path.join(bot_dir, ve, 'bin/python')
if os.path.isfile(py):
bots.append({'name': file.replace('.bot.py', ''), 'asset': False, 'executable': py})
bots.append(Bot(file.replace('.bot.py', ''), False, py))
else:
bots.append({'name': file.replace('.bot.py', ''), 'asset': False, 'executable': sys.executable})
bots.append(Bot(file.replace('.bot.py', ''), False, sys.executable))
# Also get packaged MCM bots
for file in os.listdir(self.assets()):
if file.endswith('.bot.py'):
bots.append({'name': file.replace('.bot.py', ''), 'asset': True, 'executable': sys.executable})
bots.append(Bot("Discord-MCM", True, None, discord_start, discord_stop, discord_restart, discord_status, discord_display))
return bots
def get_form(self):
bots = self.get_bots()
plugin_port = getattr(settings, 'PLUGIN_PORT', None)
screens = subprocess.getoutput("screen -ls")
form = {'screens': screens, 'bots': bots}
for bot in bots:
form[bot['name']] = True if "{0}_{1}".format(plugin_port, bot['name']) in screens else False
return form
def get(self, request):
return render(request, 'minecraft_manager/bots.html', {'current_app': 'bots', 'form': self.get_form()})
return render(request, 'minecraft_manager/bots.html', {'current_app': 'bots', 'bots': self.get_bots()})
def post(self, request):
post = request.POST
plugin_port = getattr(settings, 'PLUGIN_PORT', None)
for bot in self.get_bots():
if bot['name'] in post:
if post[bot['name']] == "stop":
subprocess.run('screen -X -S "{0}_{1}" quit'.format(plugin_port, bot['name']), shell=True)
elif post[bot['name']] in ('start', 'restart'):
subprocess.run('screen -X -S "{0}_{1}" quit'.format(plugin_port, bot['name']), shell=True)
path = self.assets() if bot['asset'] else getattr(settings, 'BOT_DIR', "")
if not path.endswith("/"):
path += "/"
print('screen -S {0}_{1} -d -m {2} {3}{1}.bot.py'.format(plugin_port, bot['name'], bot['executable'], path))
subprocess.run(
'screen -S {0}_{1} -d -m {2} {3}{1}.bot.py'.format(plugin_port, bot['name'], bot['executable'], path),
shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
return render(request, 'minecraft_manager/bots.html', {'current_app': 'bots', 'form': self.get_form()})
if bot.name in post:
if post[bot.name] == "stop":
bot.stop()
elif post[bot.name] == "start":
bot.start()
elif post[bot.name] == "restart":
bot.restart()
return render(request, 'minecraft_manager/bots.html', {'current_app': 'bots', 'bots': self.get_bots()})