diff --git a/api/api.py b/api/api.py
index fc04bfb..98ea7f2 100644
--- a/api/api.py
+++ b/api/api.py
@@ -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)
diff --git a/api/views.py b/api/views.py
index 675452e..47cd31f 100644
--- a/api/views.py
+++ b/api/views.py
@@ -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."
diff --git a/assets/bots/Discord-MCM.bot.py b/assets/bots/Discord-MCM.bot.py
deleted file mode 100644
index 092a99e..0000000
--- a/assets/bots/Discord-MCM.bot.py
+++ /dev/null
@@ -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()
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
index e69de29..9e52327 100644
--- a/bot/__init__.py
+++ b/bot/__init__.py
@@ -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"
diff --git a/bot/discord.py b/bot/discord.py
index 89f7ae5..c2a8c0b 100644
--- a/bot/discord.py
+++ b/bot/discord.py
@@ -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())
diff --git a/docs/source/django-settings.rst b/docs/source/django-settings.rst
index a7ef2fb..5ef0bea 100644
--- a/docs/source/django-settings.rst
+++ b/docs/source/django-settings.rst
@@ -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
\ No newline at end of file
diff --git a/external/views.py b/external/views.py
index e3d58ee..d6d41e1 100644
--- a/external/views.py
+++ b/external/views.py
@@ -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:
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 }}
{% endfor %}
diff --git a/utils.py b/utils.py
index 71b357a..7c94daa 100644
--- a/utils.py
+++ b/utils.py
@@ -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):
diff --git a/views.py b/views.py
index 0be02c5..3e02034 100644
--- a/views.py
+++ b/views.py
@@ -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()})