Compare commits

..

16 Commits

Author SHA1 Message Date
jolheiser aa7e4ea770 Change questions (#19)
This PR updates the questions per Discord.

Reviewed-on: #19
Co-authored-by: jolheiser <john.olheiser@gmail.com>
Co-committed-by: jolheiser <john.olheiser@gmail.com>
2022-02-15 23:01:37 +00:00
Etzelia 66cf464f3d Add discord ID and sync (#17)
UAT

Signed-off-by: Etzelia <etzelia@hotmail.com>

Optimize imports

Signed-off-by: Etzelia <etzelia@hotmail.com>

Add discord ID and sync

Signed-off-by: Etzelia <etzelia@hotmail.com>

Reviewed-on: https://git.canopymc.net/Canopy/minecraft_manager/pulls/17
Reviewed-by: ZeroHD <joey@ahines.net>
Co-Authored-By: Etzelia <etzelia@hotmail.com>
Co-Committed-By: Etzelia <etzelia@hotmail.com>
2021-07-26 15:23:37 +00:00
Etzelia 03608edfae Fix receiver for pre_delete (#12)
Fix receiver for pre_delete

Signed-off-by: Etzelia <etzelia@hotmail.com>

Reviewed-on: https://git.canopymc.net/Canopy/minecraft_manager/pulls/12
Co-Authored-By: Etzelia <etzelia@hotmail.com>
Co-Committed-By: Etzelia <etzelia@hotmail.com>
2021-07-17 01:55:45 +00:00
Etzelia 9bdacbca75 Some changes (#9)
Some changes

Signed-off-by: Etzelia <etzelia@hotmail.com>

Reviewed-on: https://git.canopymc.net/Canopy/minecraft_manager/pulls/9
Co-Authored-By: Etzelia <etzelia@hotmail.com>
Co-Committed-By: Etzelia <etzelia@hotmail.com>
2021-07-01 03:07:45 +00:00
Etzelia 7cb2a14716 You have logged out (#6)
You have logged out

Signed-off-by: Etzelia <etzelia@hotmail.com>

Reviewed-on: https://git.birbmc.com/BirbMC/minecraft_manager/pulls/6
Co-Authored-By: Etzelia <etzelia@hotmail.com>
Co-Committed-By: Etzelia <etzelia@hotmail.com>
2021-05-12 00:19:43 +00:00
Etzelia aeea1e834d attachments (#2)
Add migration for longer timezone

Signed-off-by: Etzelia <etzelia@hotmail.com>

Merge branch 'master' of birbmc.com:BirbMC/minecraft_manager into birb

# Conflicts:
#	external/views.py

Fix col

Signed-off-by: Etzelia <etzelia@hotmail.com>

Add attachments, clean up UI, etc.

Signed-off-by: Etzelia <etzelia@hotmail.com>

Fix Discord embeds (#59)

Fix Discord embeds

Reviewed-on: https://git.etztech.xyz/MMS/MinecraftManagerDjango/pulls/59
Reviewed-by: ZeroHD <joey@ahines.net>

Add requirements (not txt) (#57)

Add requirements (not txt)

Signed-off-by: Etzelia <etzelia@hotmail.com>

Reviewed-on: https://git.etztech.xyz/MMS/MinecraftManagerDjango/pulls/57
Reviewed-by: ZeroHD <joey@ahines.net>

Interim Patch (#54)

Birb Patches (#1)

Birb Patches

Signed-off-by: Etzelia <etzelia@hotmail.com>

Co-authored-by: Etzelia <etzelia@hotmail.com>

Reviewed-by: ZeroHD <joey@ahines.net>

Add 'LICENSE' (#53)

Add 'LICENSE'

Reviewed-by: ZeroHD <joey@ahines.net>

Fix captcha and generify community invite (#52)

Add validation for final question and some minor CSS

Signed-off-by: Etzelia <etzelia@hotmail.com>

Change some docs

Signed-off-by: Etzelia <etzelia@hotmail.com>

Fix captcha and generify community invite

Signed-off-by: Etzelia <etzelia@hotmail.com>

Co-authored-by: Etzelia <etzelia@hotmail.com>
Reviewed-by: ZeroHD <joey@ahines.net>

Co-authored-by: Etz Elia <etzelia@hotmail.com>
Reviewed-on: https://git.birbmc.com/BirbMC/minecraft_manager/pulls/2
Co-Authored-By: Etzelia <etzelia@hotmail.com>
Co-Committed-By: Etzelia <etzelia@hotmail.com>
2021-05-06 17:50:18 +00:00
Etzelia dead183dad
Merge branch 'captcha' of birbmc.com:BirbMC/minecraft_manager
# Conflicts:
#	models.py
#	overview.py
2021-03-15 21:09:53 -05:00
Etzelia 725ac7f40f
Add validation for final question and some minor CSS
Signed-off-by: Etzelia <etzelia@hotmail.com>
2020-06-06 12:05:57 -05:00
Etzelia a31f49c7ce
Change some docs
Signed-off-by: Etzelia <etzelia@hotmail.com>
2020-06-06 11:39:26 -05:00
Etzelia 1489b5aa7d
Fix captcha and generify community invite
Signed-off-by: Etzelia <etzelia@hotmail.com>
2020-06-06 11:37:04 -05:00
Etz Elia faa342933e Various fixes and some improvements (#49)
Rip out bot refs

Various fixes and some improvements

Signed-off-by: Etzelia <etzelia@hotmail.com>

Co-authored-by: Etzelia <etzelia@hotmail.com>
Reviewed-by: ZeroHD <joey@ahines.net>
2020-04-19 22:26:51 +02:00
Etz Elia 3bb1e910c9 Make discord bot external again (#48)
Make discord bot external again

Signed-off-by: Etzelia <etzelia@hotmail.com>

Co-authored-by: Etzelia <etzelia@hotmail.com>
Reviewed-by: ZeroHD <joey@ahines.net>
2020-04-14 21:36:31 +02:00
Etz Elia dcd1db1d4a Loop MCM bot (#47)
Loop

Signed-off-by: Etzelia <etzelia@hotmail.com>

Co-authored-by: Etzelia <etzelia@hotmail.com>
Reviewed-by: ZeroHD <joey@ahines.net>
2020-03-05 17:38:39 +01:00
Etzelia 4c9b01e42a Remove print in model POST API (#46) 2019-11-25 16:33:23 +01:00
Etzelia e91f82366d Add model POST API (#45) 2019-11-24 19:52:29 +01:00
Etzelia bc852b2090
Commit migration
Fix overview divide-by-zero error
Remove CoreProtect permissions for new app

Signed-off-by: Etzelia <etzelia@hotmail.com>
2019-07-26 12:37:20 -05:00
51 changed files with 1163 additions and 481 deletions

7
LICENSE 100644
View File

@ -0,0 +1,7 @@
Copyright 2020 Etzelia
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

15
REQUIREMENTS.md 100644
View File

@ -0,0 +1,15 @@
# Requirements
This project was built with (and should work with non-breaking upgrades of):
| Package | Version |
|-------------------------------------------------------|---------|
| [discord.py](https://pypi.org/project/discord.py/) | 1.3.4 |
| [Django](https://pypi.org/project/Django/) | 2.2 |
| [mcstatus](https://pypi.org/project/mcstatus/) | 3.1.1 |
| [mysqlclient*](https://pypi.org/project/mysqlclient/) | 1.4.6 |
| [PyYAML](https://pypi.org/project/PyYAML/) | 5.3.1 |
*or any other preferred DB

View File

@ -0,0 +1 @@
default_app_config = 'minecraft_manager.apps.MinecraftManagerAppConfig'

View File

@ -3,8 +3,8 @@ from __future__ import absolute_import
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth.models import User
from django.utils.translation import ugettext_lazy as _
from minecraft_manager.models import Application, Note, Ticket, TicketNote, Player, IP, UserSettings, Alert
from django.utils.translation import gettext as _
from minecraft_manager.models import Application, Note, Ticket, TicketNote, Player, IP, UserSettings, Alert, Attachment
from minecraft_manager.api.admin import register as api_register
@ -90,6 +90,14 @@ class IPAdmin(admin.ModelAdmin):
search_fields = ["player__username", "ip"]
class AttachmentAdmin(admin.ModelAdmin):
list_display = ["file_name", "model_name"]
def model_name(self, attachment):
return attachment.ref_name
model_name.short_description = "Model"
try:
admin.site.unregister(User)
admin.site.register(User, UserAdmin)
@ -101,6 +109,7 @@ try:
admin.site.register(Player, PlayerAdmin)
admin.site.register(IP, IPAdmin)
admin.site.register(Alert)
admin.site.register(Attachment, AttachmentAdmin)
api_register()
except admin.sites.AlreadyRegistered:
pass

View File

@ -1,5 +1,5 @@
from django.contrib import admin
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext as _
from minecraft_manager.api.models import Token

View File

@ -1,8 +1,7 @@
import socket, logging, os, datetime, pytz, mcstatus, random, string
import socket, requests, 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__)
@ -39,25 +38,31 @@ def plugin(key, command):
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)
discord_send(DestType.CHANNEL, channel_id, message, embed)
discord_mcm_webhook = getattr(settings, 'DISCORD_MCM_WEBHOOK', None)
if discord_mcm_webhook:
return post_webhook(discord_mcm_webhook, message, embed, ping)
return None
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)
discord_send(DestType.CHANNEL, channel_id, message, embed)
discord_notification_webhook = getattr(settings, 'DISCORD_NOTIFICATION_WEBHOOK', None)
if discord_notification_webhook:
return post_webhook(discord_notification_webhook, message, embed, ping)
return None
def post_webhook(webhook_url, message, embed, ping):
ping_list = getattr(settings, 'DISCORD_PING_LIST', [])
if ping and ping_list:
ping_list = ["<@&{0}>".format(ping) for ping in ping_list]
message = "{0}\n{1}".format(" ".join(ping_list), message)
data = {}
if message:
data['content'] = message
if embed:
data['embeds'] = [embed.to_dict()]
return requests.post(webhook_url, json=data)
def strip_format(message):
return message.replace("§0", "").replace("§1", "").replace("§2", "").replace("§3", "").replace("§4", "") \

View File

@ -1,22 +1,23 @@
from __future__ import absolute_import
import logging, datetime
from django.contrib.auth.forms import PasswordChangeForm
from django.contrib.auth import update_session_auth_hash
import datetime
import logging
from django.apps import apps
from django.conf import settings
from django.contrib.auth import update_session_auth_hash
from django.contrib.auth.forms import PasswordChangeForm
from django.contrib.auth.models import User
from django.forms import modelform_factory
from django.http import JsonResponse, HttpResponse
from django.utils import timezone
from django.views.generic import View
from django.forms import modelform_factory
import minecraft_manager.forms as mcm_forms
from minecraft_manager.models import Player, UserSettings, Application, IP, Ticket, Note
import minecraft_manager.api.api as mcm_api
from minecraft_manager.api.models import Token
import minecraft_manager.utils as mcm_utils
import minecraft_manager.external.stats as mcm_stats
import minecraft_manager.forms as mcm_forms
import minecraft_manager.utils as mcm_utils
from minecraft_manager.api.models import Token
from minecraft_manager.models import Player, UserSettings, Application, IP, Ticket, Note
logger = logging.getLogger(__name__)
@ -205,10 +206,11 @@ class PluginAPI(View):
application.save()
json['message'] = "Application was successfully {0}.".format(
"accepted" if post['action'] == "True" else "denied")
mcm_api.discord_mcm("{0}'s application (#{1}) was {2} by {3}".format(application.username,
link = mcm_utils.full_reverse('application_info', application.id)
mcm_api.discord_mcm("{0}'s application ([#{1}]({4})) was {2} by {3}".format(application.username,
application.id,
"accepted" if post['action'] == "True" else "denied",
post['username']))
post['username'], link))
mcm_api.plugin("accept" if post['action'] == "True" else "deny", application.username)
else:
json['status'] = False
@ -297,7 +299,7 @@ class PluginAPI(View):
ticket = Ticket(player=player, message=post['message'], x=post['x'], y=post['y'], z=post['z'], world=post['world'])
ticket.save()
json['message'] = "Ticket submitted."
link = "{}".format(mcm_utils.url_path(settings.MCM_BASE_LINK, 'dashboard/ticket', ticket.id))
link = mcm_utils.full_reverse('ticket_info', ticket.id)
msg = mcm_utils.build_ticket(ticket, link)
json['extra'] = {'id': ticket.id, 'link': link}
mcm_api.discord_mcm(embed=msg, ping=True)
@ -311,8 +313,8 @@ class PluginAPI(View):
warning = Note(player=player, message=post['message'], importance=post['severity'], staff=staff.auth_user)
warning.save()
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)
link = mcm_utils.full_reverse('note_info', warning.id)
msg = mcm_utils.build_note(warning, link)
mcm_api.discord_mcm(embed=msg)
except Exception as ex:
json['status'] = False
@ -390,7 +392,7 @@ class ModelAPI(View):
json = []
for value in objects:
try:
link = "{}".format(mcm_utils.url_path(settings.MCM_BASE_LINK, 'dashboard', request_model, value['id']))
link = mcm_utils.full_reverse(f"{request_model}_info", value['id'])
value['link'] = link
except:
pass
@ -398,7 +400,31 @@ class ModelAPI(View):
return JsonResponse(json, safe=False)
def post(self, request, request_model):
pass
json = {"success": False, "message": ""}
if request_allowed(request, 'model_post_permission'):
post = request.POST
model = None
for m in apps.get_app_config('minecraft_manager').get_models():
if m._meta.model_name.upper() == request_model.upper():
model = m
break
if model:
keywords = clean(model, post)
if "id" in keywords:
try:
obj = model.objects.get(id=keywords["id"])
for key in keywords.keys():
setattr(obj, key, keywords[key])
obj.save()
json["success"] = True
json["message"] = "Model updated"
except Exception as ex:
print(ex)
json["message"] = "Could not update model"
else:
json["message"] = "Must provide an ID"
return JsonResponse(json)
class StatsAPI(View):

7
apps.py 100644
View File

@ -0,0 +1,7 @@
from django.apps import AppConfig
class MinecraftManagerAppConfig(AppConfig):
name = 'minecraft_manager'
verbose_name = "Minecraft Manager"

View File

@ -1,38 +0,0 @@
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

@ -0,0 +1,29 @@
import os
import sys
import django
# This block is assuming you will use this exact file
sep = os.sep
path = os.path.dirname(os.path.abspath(__file__))
path = path.split(sep)[:-3]
project = path[-1]
path = sep.join(path)
# What you need here is
# project = name of your main django project
# path = path to the root of your django project
# e.g. If your project is at /home/mcm/django1 and settings.py is at /home/mcm/django1/django2/settings.py
# project = django2
# path = /home/mcm/django1
sys.path.append(path)
print("Setting path for {0}: {1}".format(project, path))
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "{}.settings".format(project))
django.setup()
from django.conf import settings
from minecraft_manager.bot.discord import Discord
token = getattr(settings, 'DISCORD_BOT_TOKEN', None)
bot = Discord(token)
bot.run_bot()

View File

@ -1,12 +1,13 @@
import discord
from io import StringIO
from discord.ext import commands
from django.contrib.auth.models import User
from django.db import close_old_connections
from minecraft_manager.api import api
from minecraft_manager.bot.utils import get_application
from minecraft_manager.utils import build_application
from minecraft_manager.models import Application, Player
from minecraft_manager.utils import build_application, full_static
class Commands(commands.Cog):
@ -44,8 +45,8 @@ class Commands(commands.Cog):
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="-----------------------------")
url=full_static('favicon.png'))
embed.title = "Minecraft Manager Help"
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),
@ -54,6 +55,7 @@ class Commands(commands.Cog):
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.")
embed.add_field(name="{}sync".format(self.bot.prefix), value="Sync Discord users with Player records.")
await self.bot.discord_message(ctx.message.channel, embed)
@commands.group("app", aliases=["application"])
@ -153,6 +155,29 @@ class Commands(commands.Cog):
if not api.plugin(api.PLUGIN_DENY, application.username):
await self.bot.discord_message(ctx.message.channel, "Could not deny in-game, is the server running?")
@commands.command()
async def clear(self, ctx, app_id: int):
await self._clear(ctx, app_id)
@app.command("clear")
async def app_clear(self, ctx, app_id: int):
await self._clear(ctx, app_id)
async def _clear(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 = None
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 cleared.".format(app_id))
@commands.command()
async def search(self, ctx, search: str):
await self._search(ctx, search)
@ -235,6 +260,31 @@ class Commands(commands.Cog):
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)))
@commands.command()
async def sync(self, ctx):
def sync_player(player):
for member in ctx.guild.members:
name = member.nick if member.nick else member.name
if player.username.lower() == name.lower():
player.discord_id = member.id
player.save()
return True
return False
# Attempt to sync users
need_sync = Player.objects.filter(discord_id__isnull=True)
need_sync_count = need_sync.count()
synced = []
not_synced = []
if need_sync_count > 0:
for ns in need_sync:
if sync_player(ns):
synced.append(ns.username)
else:
not_synced.append(ns.username)
txt = "Synced\n\t{}\n\nNot Synced\n\t{}".format('\n\t'.join(synced), '\n\t'.join(not_synced))
attach = discord.File(fp=StringIO(txt), filename="sync.txt")
await ctx.channel.send(content="Successfully synced {}/{} players.".format(len(synced), need_sync_count), file=attach)
def setup(bot):
bot.add_cog(Commands(bot))

View File

@ -1,41 +1,21 @@
import asyncio
import logging
import traceback
import threading
from enum import Enum
import discord
from discord.ext import commands
from django.conf import settings
from django.shortcuts import reverse
from minecraft_manager.models import Application, Ticket
from minecraft_manager.utils import url_path
from minecraft_manager.utils import full_reverse
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', '!')
@ -43,14 +23,15 @@ class Discord(commands.Bot):
superuser_roles = getattr(settings, 'DISCORD_SUPERUSER_ROLES', [])
error_users = getattr(settings, 'DISCORD_ERROR_USERS', [])
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)
def __init__(self, token):
intents = discord.Intents.default()
intents.members = True
super().__init__(command_prefix=self.prefix, description=description, case_insensitive=True, help_command=None,
activity=discord.Game(name=self.discord_game), intents=intents)
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)
@ -67,11 +48,11 @@ class Discord(commands.Bot):
content = ""
unanswered_applications = Application.objects.filter(accepted=None)
if len(unanswered_applications) > 0:
link = url_path(settings.MCM_BASE_LINK, 'dashboard/application')
link = full_reverse('application')
content += "[Unanswered Applications: {}]({})".format(len(unanswered_applications), link)
unclaimed_tickets = Ticket.objects.filter(staff=None, resolved=False)
if len(unclaimed_tickets) > 0:
link = url_path(settings.MCM_BASE_LINK, 'dashboard/ticket')
link = full_reverse('ticket')
if content:
content += "\n\n"
content += "[Unclaimed Tickets: {}]({})".format(len(unclaimed_tickets), link)
@ -80,10 +61,6 @@ class Discord(commands.Bot):
embed.description = content
await self.discord_message(channel, embed)
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):
@ -107,77 +84,14 @@ class Discord(commands.Bot):
await self.discord_message(user, '```python\n{}```'.format(error))
def run_bot(self):
global discord_loop
loop = asyncio.get_event_loop()
try:
discord_loop.run_until_complete(self.start(self.token))
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)
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())
finally:
loop.run_until_complete(self.close())
logger.info("Bot shutting down...")

29
discord.py 100644
View File

@ -0,0 +1,29 @@
from enum import Enum
from datetime import datetime
from typing import Union
class TimestampStyle(Enum):
SHORT_TIME = "t"
"""16:20"""
LONG_TIME = "T"
"""16:20:30"""
SHORT_DATE = "d"
"""20/04/2021"""
LONG_DATE = "D"
"""20 April 2021"""
SHORT_DATETIME = "f"
"""20 April 2021 16:20"""
LONG_DATETIME = "F"
"""Tuesday, 20 April 2021 16:20"""
RELATIVE = "R"
"""2 months ago"""
def format_timestamp(unix: Union[int, datetime], style: TimestampStyle = TimestampStyle.SHORT_DATETIME) -> str:
t = 0
if isinstance(unix, int):
t = unix
elif isinstance(unix, datetime):
t = unix.timestamp()
return f"<t:{t}:{style}>"

View File

@ -23,6 +23,10 @@ Optional
``BOT_DIR`` - The path to your bot directory.
``DISCORD_NOTIFICATION_WEBHOOK`` - The URL for the webhook used for notifications.
``DISCORD_MCM_WEBHOOK`` - The URL for the webhook used for Applications, Tickets, and Warnings.
``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.
@ -39,7 +43,9 @@ Optional
``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.
``INVITE_LINK`` - The invite link to your community.
``INVITE_LABEL`` - The invite label for your community.
``DYNMAP_URL`` - The URL to your dynmap if you have one. Leave blank if you'd rather use a static background for web forms.

12
external/urls.py vendored
View File

@ -1,10 +1,10 @@
from django.conf.urls import url
from django.contrib.auth.decorators import login_required
from django.urls import path
import minecraft_manager.external.views as external
urlpatterns = [
url(r'^apply/$', external.Apply.as_view(), name="external-apply"),
url(r'^ticket/$', external.Ticket.as_view(), name="external-ticket"),
url(r'^stats/$', external.Stats.as_view(), name="external-stats"),
url(r'^stats/player/$', external.StatsPlayer.as_view(), name="external-stats-player"),
path('apply/', external.Apply.as_view(), name="external-apply"),
path('ticket/', external.Ticket.as_view(), name="external-ticket"),
path('stats/', external.Stats.as_view(), name="external-stats"),
path('stats/player/', external.StatsPlayer.as_view(), name="external-stats-player"),
]

41
external/views.py vendored
View File

@ -1,5 +1,5 @@
from django.views.generic import View
from django.shortcuts import render, reverse, redirect
from django.shortcuts import render, reverse
from django.conf import settings
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt
@ -8,12 +8,13 @@ import minecraft_manager.api.api as mcm_api
import minecraft_manager.utils as mcm_utils
import minecraft_manager.external.stats as mcm_stats
from minecraft_manager.models import Player
import random, yaml, os, json, datetime, pytz
import random, yaml, os
def config():
data = {}
data['discord_invite'] = getattr(settings, "DISCORD_INVITE", "#")
data['invite_link'] = getattr(settings, "INVITE_LINK", "#")
data['invite_label'] = getattr(settings, "INVITE_LABEL", "community")
dynmap_url = getattr(settings, "DYNMAP_URL", "")
data['dynmap_url'] = dynmap_url
@ -45,12 +46,16 @@ def config():
def rules():
path = os.path.join(settings.MINECRAFT_BASE_DIR, "plugins/MinecraftManager/config.yml")
with open(path) as config_file:
config = yaml.load(config_file)
data = config['rules']['rules']
if config['rules']['application']['validate']:
data.append("The answer to the final question is \"{}\"".format(config['rules']['application']['answer']))
cfg = yaml.safe_load(config_file)
data = cfg['rules']['rules']
if cfg['rules']['application']['validate']:
data.append("The answer to the final question is \"{}\"".format(cfg['rules']['application']['answer']))
return data
return {
"rules": data,
"validate": cfg['rules']['application']['validate'],
"answer": cfg['rules']['application']['answer']
}
@method_decorator(csrf_exempt, name='dispatch')
@ -59,15 +64,17 @@ class Apply(View):
def get(self, request):
form = ApplicationForm()
return render(request, 'minecraft_manager/external/apply.html',
{'form': form.as_p(), 'rules': rules(), 'valid': False, 'map': config(),
'captcha': hasattr(settings, "CAPTCHA_SECRET")})
{'form': form.as_p(), 'rules': rules()["rules"], 'valid': False, 'map': config(),
'captcha': getattr(settings, "CAPTCHA_SITE", "")})
def post(self, request):
form = ApplicationForm(request.POST)
valid_username = mcm_utils.validate_username(form.data['username'])
captcha = mcm_utils.Captcha(request.POST)
valid = form.is_valid()
if valid and valid_username and captcha.success:
r = rules()
valid_answer = not r["validate"] or r["answer"].casefold() == form.data['read_rules'].casefold()
if valid and valid_username and valid_answer and captcha.success:
app = form.save()
msg = mcm_utils.build_application(app)
mcm_api.discord_mcm(message='New Application!', embed=msg)
@ -77,9 +84,11 @@ class Apply(View):
form.add_error(None, error)
if not valid_username:
form.add_error(None, "That username is not a premium Minecraft account")
if not valid_answer:
form.add_error(None, "Please read the rules again")
return render(request, 'minecraft_manager/external/apply.html',
{'form': form.as_p(), 'rules': rules(), 'valid': valid and valid_username and captcha.success, 'map': config(),
'captcha': hasattr(settings, "CAPTCHA_SECRET")})
{'form': form.as_p(), 'rules': r["rules"], 'valid': valid and valid_username and valid_answer and captcha.success, 'map': config(),
'captcha': getattr(settings, "CAPTCHA_SITE", "")})
@method_decorator(csrf_exempt, name='dispatch')
@ -89,7 +98,7 @@ class Ticket(View):
form = TicketForm()
return render(request, 'minecraft_manager/external/ticket.html',
{'form': form.as_p(), 'valid': False, 'map': config(),
'captcha': hasattr(settings, "CAPTCHA_SECRET")})
'captcha': getattr(settings, "CAPTCHA_SITE", "")})
def post(self, request):
post = request.POST.copy()
@ -107,7 +116,7 @@ class Ticket(View):
if valid and captcha.success:
ticket = form.save()
# Create the message to send to Discord
link = "{}".format(mcm_utils.url_path(settings.MCM_BASE_LINK, 'dashboard/ticket', ticket.id))
link = mcm_utils.full_reverse('ticket_info', ticket.id)
msg = mcm_utils.build_ticket(ticket, link)
mcm_api.discord_mcm(message="New Ticket", embed=msg, ping=True)
mcm_api.plugin("ticket", "{0} {1} {2}".format(username, ticket.id, link))
@ -120,7 +129,7 @@ class Ticket(View):
form.data['player'] = username
return render(request, 'minecraft_manager/external/ticket.html',
{'form': form.as_p(), 'valid': valid and captcha.success, 'map': config(),
'captcha': hasattr(settings, "CAPTCHA_SECRET")})
'captcha': getattr(settings, "CAPTCHA_SITE", "")})
@method_decorator(csrf_exempt, name='dispatch')

View File

@ -30,7 +30,7 @@ class TicketForm(ModelForm):
fields = ['player', 'message', 'priority', 'world', 'x', 'y', 'z']
widgets = {
'player': TextInput,
'message': Textarea,
'message': Textarea(attrs={'style': 'display: block;'}),
}
@ -39,7 +39,7 @@ class NoteForm(ModelForm):
model = Note
fields = ['player', 'message', 'importance']
widgets = {
'message': Textarea
'message': Textarea(attrs={'style': 'display: block;'})
}

View File

@ -13,4 +13,4 @@ class TimezoneMiddleware(MiddlewareMixin):
try:
timezone.activate(pytz.timezone(request.user.usersettings.default_timezone))
except:
timezone.deactivate()
timezone.deactivate()

View File

@ -0,0 +1,29 @@
# Generated by Django 2.2.3 on 2021-02-06 21:40
from django.db import migrations, models
import minecraft_manager.models
class Migration(migrations.Migration):
dependencies = [
('minecraft_manager', '0014_auto_20190930_1103'),
]
operations = [
migrations.CreateModel(
name='Attachment',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('ref_model', models.CharField(choices=[('T', 'Ticket Note'), ('N', 'Note')], max_length=1)),
('ref_id', models.IntegerField()),
('file', models.FileField(upload_to=minecraft_manager.models._attachment_upload_to)),
('created', models.DateTimeField(auto_now_add=True)),
],
),
migrations.AlterField(
model_name='application',
name='reference',
field=models.CharField(blank=True, max_length=50, null=True, verbose_name='How did you find out about our server?'),
),
]

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.3 on 2021-07-20 23:29
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('minecraft_manager', '0016_auto_20210328_0131'),
]
operations = [
migrations.AddField(
model_name='player',
name='discord_id',
field=models.CharField(blank=True, max_length=30, null=True, unique=True),
),
]

View File

@ -1,9 +1,17 @@
from django.db import models
from django.contrib.auth.models import User
from django.db.models import Q
import logging, yaml, pytz, json, os
from django.conf import settings
import json
import logging
import os
from datetime import datetime
from os.path import basename
import pytz
import yaml
from django.conf import settings
from django.contrib.auth.models import User
from django.db import models
from django.db.models import Q
from django.db.models.signals import pre_delete
from django.dispatch import receiver
logger = logging.getLogger(__name__)
@ -39,7 +47,7 @@ class UserSettings(models.Model):
auth_user = models.OneToOneField(User, on_delete=models.CASCADE, null=True, blank=True)
default_results = models.SmallIntegerField("Default Results", default=10, choices=RESULT_OPTIONS)
default_theme = models.CharField("Theme", max_length=2, default='DE', choices=THEME_OPTIONS)
default_timezone = models.CharField("Timezone", max_length=20, default='UTC', choices=TIMEZONES)
default_timezone = models.CharField("Timezone", max_length=50, default='UTC', choices=TIMEZONES)
search_player_ip = models.BooleanField("Include IP in Player search", default=False)
show_timestamp_chat = models.BooleanField("Show Timestamp By Chat", default=False)
last_ip = models.CharField(max_length=30, default="127.0.0.1", editable=False)
@ -58,12 +66,12 @@ class UserSettings(models.Model):
class Application(models.Model):
username = models.CharField("Minecraft Username", max_length=20, unique=True)
age = models.PositiveSmallIntegerField()
player_type = models.TextField("What type of player are you?", max_length=300)
player_type = models.TextField("What's your favorite thing to do in Minecraft?", max_length=300)
ever_banned = models.BooleanField("Have you ever been banned?", default=False)
ever_banned_explanation = models.TextField("If you were previously banned, will you share why?", max_length=300, blank=True, null=True)
reference = models.CharField("Were you referred to our server?", max_length=50, blank=True, null=True)
reference = models.CharField("How did you find out about us? Please give a website or player name, if applicable.", max_length=50, blank=True, null=True)
read_rules = models.CharField("Have you read the rules?", max_length=10)
accepted = models.NullBooleanField()
accepted = models.BooleanField(null=True)
date = models.DateTimeField(auto_now_add=True, blank=True, null=True)
objects = models.Manager()
@ -97,7 +105,7 @@ class Player(models.Model):
application = models.ForeignKey(Application, on_delete=models.SET_NULL, null=True, blank=True)
first_seen = models.DateField(null=True, blank=True)
last_seen = models.DateField(null=True, blank=True)
discord_id = models.CharField(max_length=30, unique=True, null=True, blank=True)
@property
def is_banned(self):
@ -252,6 +260,10 @@ class Ticket(models.Model):
def date_display(self):
return str(self.date).split(".")[0]
@property
def notes(self):
return TicketNote.objects.filter(ticket=self)
def __str__(self):
return "{}: {}".format(self.issuer, self.snippet)
@ -263,6 +275,10 @@ class TicketNote(models.Model):
last_update = models.DateTimeField(auto_now_add=True, blank=True, null=True)
date = models.DateTimeField(auto_now_add=True, blank=True, null=True)
@property
def attachments(self):
return Attachment.objects.filter(ref_model=RefModels.TICKET_NOTE[0], ref_id=self.id)
class Meta:
verbose_name = "Ticket Note"
verbose_name_plural = "Ticket Notes"
@ -324,10 +340,54 @@ class Note(models.Model):
def date_display(self):
return str(self.date).split(".")[0]
@property
def attachments(self):
return Attachment.objects.filter(ref_model=RefModels.NOTE[0], ref_id=self.id)
def __str__(self):
return "{}: {}".format(self.issuee, self.snippet)
class RefModels:
TICKET_NOTE = 'T', 'Ticket Note'
NOTE = 'N', 'Note'
choices = TICKET_NOTE, NOTE
@staticmethod
def label(ref: str):
for c in RefModels.choices:
if c[0] == ref:
return c[1]
return "None"
def _attachment_upload_to(instance, filename):
return f"attachments/{instance.ref_name.lower().replace(' ', '_')}s/{instance.ref_id}/{filename}"
class Attachment(models.Model):
ref_model = models.CharField(max_length=1, choices=RefModels.choices)
ref_id = models.IntegerField()
file = models.FileField(upload_to=_attachment_upload_to)
created = models.DateTimeField(auto_now_add=True)
@property
def ref_name(self):
return RefModels.label(self.ref_model)
@property
def file_name(self):
return basename(self.file.name)
def __str__(self):
return self.file.name
@receiver(pre_delete, sender=Attachment, dispatch_uid="delete_attachments")
def attachment_delete(sender, instance, **kwargs):
instance.file.delete(False)
class IPManager(models.Manager):
def get_queryset(self):
users = User.objects.filter(is_active=True)

View File

@ -1,4 +1,9 @@
@import url("https://fonts.googleapis.com/css?family=Lato:400,700,400italic");
/* Custom CSS */
.nav-sidebar {
border-right: 1px solid #464545;
}
/*!
* bootswatch v3.3.7
* Homepage: http://bootswatch.com

View File

@ -1,4 +1,9 @@
@import url("https://fonts.googleapis.com/css?family=Lato:400,700,400italic");
/* Custom CSS */
.nav-sidebar {
border-right: 1px solid #ecf0f1;
}
/*!
* bootswatch v3.3.7
* Homepage: http://bootswatch.com

View File

@ -1,3 +1,9 @@
@import url("https://fonts.googleapis.com/css?family=Roboto:400,700");
/* Custom CSS */
.nav-sidebar {
border-right: 1px solid #1c1e22;
}
/*!
* bootswatch v3.3.7
* Homepage: http://bootswatch.com

View File

@ -1,4 +1,9 @@
@import url("https://fonts.googleapis.com/css?family=Roboto:400,700");
/* Custom CSS */
.nav-sidebar {
border-right: 1px solid #282828;
}
/*!
* bootswatch v3.3.7
* Homepage: http://bootswatch.com

View File

@ -3,22 +3,19 @@
min-height:100%;
height:auto!important;
position:fixed;
top:0px;
left:0px;
top:0;
left:0;
overflow:hidden;
border:0px;
border:0;
z-index:-9;
float:left;
}
#form {
background: rgba(255, 255, 255, .9);
margin: auto;
background: rgba(0, 0, 0, .75);
display: block;
width: 35%;
padding-top: 1em;
padding-bottom: 1em;
padding-left: 5em;
width: 75%;
padding: 1em 5em;
border-radius: 1em;
top: 50%;
left: 50%;
@ -65,6 +62,15 @@
color: black;
}
.sub {
margin-left: 1em;
}
.rule {
margin-bottom: .5em;
}
.errorlist {
color: #D8000C;
background-color: #FFD2D2;
}

View File

@ -0,0 +1,249 @@
/* Sakura.css v1.3.1
* ================
* Minimal css theme.
* Project: https://github.com/oxalorg/sakura/
*/
/* Default Sakura Theme */
:root {
--color-blossom: #1d7484;
--color-fade: #982c61;
--color-bg: #f9f9f9;
--color-bg-alt: #f1f1f1;
--color-text: #4a4a4a;
}
/* Sakura Dark Theme */
@media (prefers-color-scheme: dark) {
:root {
--color-blossom: #ffffff;
--color-fade: #c9c9c9;
--color-bg: #222222;
--color-bg-alt: #4a4a4a;
--color-text: #c9c9c9;
}
}
html {
font-size: 62.5%;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
}
body {
font-size: 1.8rem;
line-height: 1.618;
max-width: 38em;
margin: auto;
color: var(--color-text);
background-color: var(--color-bg);
padding: 13px;
}
@media (max-width: 684px) {
body {
font-size: 1.53rem;
}
}
@media (max-width: 382px) {
body {
font-size: 1.35rem;
}
}
h1, h2, h3, h4, h5, h6 {
line-height: 1.1;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
font-weight: 700;
margin-top: 3rem;
margin-bottom: 1.5rem;
overflow-wrap: break-word;
word-wrap: break-word;
-ms-word-break: break-all;
word-break: break-word;
}
h1 {
font-size: 2.35em;
}
h2 {
font-size: 2.00em;
}
h3 {
font-size: 1.75em;
}
h4 {
font-size: 1.5em;
}
h5 {
font-size: 1.25em;
}
h6 {
font-size: 1em;
}
p {
margin-top: 0px;
margin-bottom: 2.5rem;
}
small, sub, sup {
font-size: 75%;
}
hr {
border-color: var(--color-blossom);
}
a {
text-decoration: none;
color: var(--color-blossom);
}
a:hover {
color: var(--color-fade);
border-bottom: 2px solid var(--color-text);
}
a:visited {
color: var(--color-blossom);
}
ul {
padding-left: 1.4em;
margin-top: 0px;
margin-bottom: 2.5rem;
}
li {
margin-bottom: 0.4em;
}
blockquote {
margin-left: 0px;
margin-right: 0px;
padding-left: 1em;
padding-top: 0.8em;
padding-bottom: 0.8em;
padding-right: 0.8em;
border-left: 5px solid var(--color-blossom);
margin-bottom: 2.5rem;
background-color: var(--color-bg-alt);
}
blockquote p {
margin-bottom: 0;
}
img, video {
height: auto;
max-width: 100%;
margin-top: 0px;
margin-bottom: 2.5rem;
}
/* Pre and Code */
pre {
background-color: var(--color-bg-alt);
display: block;
padding: 1em;
overflow-x: auto;
margin-top: 0px;
margin-bottom: 2.5rem;
}
code {
font-size: 0.9em;
padding: 0 0.5em;
background-color: var(--color-bg-alt);
white-space: pre-wrap;
}
pre > code {
padding: 0;
background-color: transparent;
white-space: pre;
}
/* Tables */
table {
text-align: justify;
width: 100%;
border-collapse: collapse;
}
td, th {
padding: 0.5em;
border-bottom: 1px solid var(--color-bg-alt);
}
/* Buttons, forms and input */
input, textarea {
border: 1px solid var(--color-text);
}
input:focus, textarea:focus {
border: 1px solid var(--color-blossom);
}
textarea {
width: 100%;
}
.button, button, input[type="submit"], input[type="reset"], input[type="button"] {
display: inline-block;
padding: 5px 10px;
text-align: center;
text-decoration: none;
white-space: nowrap;
background-color: var(--color-blossom);
color: var(--color-bg);
border-radius: 1px;
border: 1px solid var(--color-blossom);
cursor: pointer;
box-sizing: border-box;
}
.button[disabled], button[disabled], input[type="submit"][disabled], input[type="reset"][disabled], input[type="button"][disabled] {
cursor: default;
opacity: .5;
}
.button:focus:enabled, .button:hover:enabled, button:focus:enabled, button:hover:enabled, input[type="submit"]:focus:enabled, input[type="submit"]:hover:enabled, input[type="reset"]:focus:enabled, input[type="reset"]:hover:enabled, input[type="button"]:focus:enabled, input[type="button"]:hover:enabled {
background-color: var(--color-fade);
border-color: var(--color-fade);
color: var(--color-bg);
outline: 0;
}
textarea, select, input {
color: var(--color-text);
padding: 6px 10px;
/* The 6px vertically centers text on FF, ignored by Webkit */
margin-bottom: 10px;
background-color: var(--color-bg-alt);
border: 1px solid var(--color-bg-alt);
border-radius: 4px;
box-shadow: none;
box-sizing: border-box;
}
textarea:focus, select:focus, input:focus {
border: 1px solid var(--color-blossom);
outline: 0;
}
input[type="checkbox"]:focus {
outline: 1px dotted var(--color-blossom);
}
label, legend, fieldset {
display: block;
margin-bottom: .5rem;
font-weight: 600;
}

View File

@ -4,7 +4,7 @@
{% block title %}Alerts{% endblock %}
{% block section %}
<div id="content" hidden="hidden">
<table id="model-table" class="table table-hover link-table">
<table id="model-table" class="table table-striped table-hover link-table">
<thead>
<tr>
<th>Message</th>
@ -14,9 +14,15 @@
</thead>
<tbody>
{% for alert in alerts %}
<tr {% if alert.seen is True %}class="success"{% endif %} data-id="{{ alert.id }}">
<tr data-id="{{ alert.id }}">
<td>{{ alert.snippet }}</td>
<td>{{ alert.seen }}</td>
<td>
{% if alert.seen %}
<span><i class="glyphicon glyphicon-ok-circle text-success"></i></span>
{% else %}
<span><i class="glyphicon glyphicon-remove-circle text-danger"></i></span>
{% endif %}
</td>
<td>{{ alert.date }}</td>
</tr>
{% endfor %}

View File

@ -3,7 +3,7 @@
{% block title %}Applications{% endblock %}
{% block section %}
<div id="content" hidden="hidden">
<table id="model-table" class="table table-hover link-table">
<table id="model-table" class="table table-striped table-hover link-table">
<thead>
<tr>
<th>App ID</th>
@ -16,18 +16,25 @@
</thead>
<tbody>
{% for app in applications %}
<tr {% if app.accepted is True %}class="success"{% endif %}{% if app.accepted is False %}class="danger"{% endif %} data-id="{{ app.id }}">
<tr data-id="{{ app.id }}">
<td>{{ app.id }}</td>
<td>{{ app.username }}</td>
<td>{{ app.age }}</td>
<td>{{ app.ever_banned }}</td>
<td>{{ app.status }}</td>
<td>
{{ app.status }}
{% if app.accepted is True %}
<span><i class="glyphicon glyphicon-ok-circle text-success"></i></span>
{% elif app.accepted is False %}
<span><i class="glyphicon glyphicon-remove-circle text-danger"></i></span>
{% endif %}
</td>
<td>{{ app.date }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<a href="{% url 'reference' %}">Reference Report</a>
<a class="btn btn-primary" href="{% url 'reference' %}">Reference Report</a>
</div>
<script>
$(document).ready(function() {

View File

@ -9,7 +9,7 @@
<!-- <p>Username: {{ application.username }}</p> -->
<p>Age: {{ application.age }}</p>
<p>Application Date: {{ application.date }}</p>
<p>Type Of Player:</p>
<p>Favorite Activity:</p>
<p class="well">{{ application.player_type }}</p>
{% if application.accepted is not null %}
<p>Status: {{ application.status }}</p>
@ -26,7 +26,7 @@
<p>Explanation: {{ application.ever_banned_explanation }}</p>
{% endif %}
{% if application.reference %}
<p>Reference: {{ application.reference }}</p>
<p>Referral: {{ application.reference }}</p>
{% endif %}
<p>Read The Rules: {{ application.read_rules }}</p>
</div>

View File

@ -2,7 +2,7 @@
{% block title %}Bans{% endblock %}
{% block section %}
<div id="content" hidden="hidden">
<table id="model-table" class="table table-hover link-table">
<table id="model-table" class="table table-striped table-hover link-table">
<thead>
<tr>
<th>Player</th>

View File

@ -1,16 +0,0 @@
{% extends "minecraft_manager/dashboard.html" %}
{% load csrf_html %}
{% block title %}Bots{% endblock %}
{% block section %}
<div id="content">
{% 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 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 %}
</div>
{% endblock section %}

View File

@ -28,6 +28,9 @@
<li class="dropdown">
<a class="dropdown-toggle " href="#" role="button" data-toggle="dropdown" id="accountDropdown">{{ user.username }}</a>
<ul class="dropdown-menu" role="menu" aria-labelledby="accountDropdown">
{% if user.is_staff %}
<li><a target="_blank" href="{% url "admin:index" %}">Admin Site</a></li>
{% endif %}
<li><a style="cursor:pointer;" data-toggle="modal" data-target="#settingsModal">Settings</a></li>
<li><a style="cursor:pointer;" data-toggle="modal" data-target="#passwordModal">Change Password</a></li>
<li><a href="{% url "logout" %}">Logout</a></li>
@ -69,9 +72,6 @@
<form id="settingsForm" method="POST" action="{% url 'api-web' keyword='settings' %}">{% autoescape off %}{% get_csrf_html request %}{% endautoescape %}
<div class="modal-body">
{% get_form 'usersettings' request %}
{% if user.is_staff %}
<br/><a target="_blank" href="{% url "admin:index" %}">Admin Site</a>
{% endif %}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>

View File

@ -1,4 +1,5 @@
{% extends 'minecraft_manager/external/base.html' %}
{% load str_utils %}
{% block title %}Application Form{% endblock %}
@ -7,7 +8,7 @@
{% block form_top %}
<h3>Rules</h3>
{% for rule in rules %}
<div class="rule">{{ rule }}</div>
<div class="{% if rule|has_prefix:'*' %}sub {% endif %}rule">{{ rule }}</div>
{% endfor %}
{% endblock %}
@ -17,7 +18,7 @@
<br/>
We will get back to you soon.
<br/>
Consider joining our <a href="https://discord.gg/{{ map.discord_invite }}">Discord</a></h2>
Consider joining our <a href="{{ map.invite_link }}">{{ map.invite_label }}</a></h2>
{% endblock %}
{% endif %}

View File

@ -10,6 +10,7 @@
<script src='https://www.google.com/recaptcha/api.js'></script>
{% endif %}
<link rel="stylesheet" href="{% static 'minecraft_manager/css/external.css' %}">
<link rel="stylesheet" href="{% static 'minecraft_manager/css/sakura.css' %}">
<link rel="stylesheet" href="//cdn.datatables.net/1.10.19/css/jquery.dataTables.min.css">
</head>
<body>
@ -21,7 +22,7 @@
{% else %}
<form method="{% block method %}POST{% endblock %}">
{% block form %}{{ form }}{% endblock %}<br/><br/>
{% if captcha %}<div class="g-recaptcha" data-sitekey="6LeLcGEUAAAAAMMpHR-7VL6Q3nNV5v03S5sq1Ddz"></div>{% endif %}
{% if captcha %}<div class="g-recaptcha" data-sitekey="{{ captcha }}"></div>{% endif %}
<button type="submit">{% block submit %}Submit{% endblock %}</button>
</form>
{% endif %}

View File

@ -3,7 +3,7 @@
{% block content %}
<h1>Logged out</h1>
<p>You have logged out 24CarrotCraft.</p><p>
<p>You have logged out.</p><p>
</p><p><a href="{% url 'login' %}">Log back in</a>. </p>
{% endblock %}

View File

@ -0,0 +1,47 @@
<div id="addAttachmentModal" class="modal fade" role="dialog">
<div class="modal-dialog">
<!-- Modal content-->
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h4 class="modal-title">Add Attachment(s)</h4>
</div>
<div class="modal-body">
<form method="post" id="add-attachment-form" enctype="multipart/form-data">{% csrf_token %}
<input type="file" multiple name="attachments"/>
<input type="hidden" name="next" value="{% url 'note_info' note.id %}"/>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" id="add-attachment-save">Save</button>
</div>
</div>
</div>
</div>
<script>
$(document).ready(() => {
const $modal = $('#addAttachmentModal');
const addAttachmentSave = document.querySelector('#add-attachment-save');
const addAttachmentForm = document.querySelector('#add-attachment-form');
document.querySelectorAll('.add-attachment[data-model][data-ref]').forEach((elem) => {
elem.addEventListener('click', () => {
addAttachmentModal(elem.dataset.model, elem.dataset.ref);
});
});
$modal.on('hidden.bs.modal', () => {
addAttachmentForm.reset();
});
addAttachmentSave.addEventListener('click', () => {
addAttachmentForm.submit();
});
function addAttachmentModal(model, ref) {
addAttachmentForm.action = '{% url 'attachment_add' 'X' 0 %}'
.replace('X', model)
.replace('0', ref);
$modal.modal('show');
}
});
</script>

View File

@ -0,0 +1,48 @@
<div id="deleteAttachmentModal" class="modal fade" role="dialog">
<div class="modal-dialog">
<!-- Modal content-->
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h4 class="modal-title">Delete Attachment</h4>
</div>
<div class="modal-body">
<p>Are you sure you want to delete <code id="delete-attachment-name"></code>?</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" id="delete-attachment-confirm">Confirm</button>
</div>
</div>
</div>
</div>
<script>
$(document).ready(() => {
const $modal = $('#deleteAttachmentModal');
const deleteAttachmentName = document.querySelector('#delete-attachment-name');
const deleteAttachmentConfirm = document.querySelector('#delete-attachment-confirm');
document.querySelectorAll('.delete-attachment[data-name][data-id]').forEach((elem) => {
elem.addEventListener('click', () => {
deleteAttachmentModal(elem.dataset.name, elem.dataset.id);
});
});
deleteAttachmentConfirm.addEventListener('click', () => {
const attachmentURL = '{% url 'attachment' 0 %}'.replace('0', deleteAttachmentConfirm.dataset.id);
fetch(attachmentURL, {
method: 'DELETE',
headers: {
'X-CSRFToken': '{{ csrf_token }}'
}
}).then(() => {
location.reload();
});
});
function deleteAttachmentModal(name, id) {
deleteAttachmentName.innerHTML = name;
deleteAttachmentConfirm.dataset.id = id;
$modal.modal('show');
}
});
</script>

View File

@ -2,24 +2,34 @@
{% block title %}Notes{% endblock %}
{% block section %}
<div id="content" hidden="hidden">
<table id="model-table" class="table table-hover link-table">
<table id="model-table" class="table table-striped table-hover link-table">
<thead>
<tr>
<th>Player</th>
<th>Message</th>
<th>Importance</th>
<td>Issued By</td>
<td>Attachments</td>
<th>Date</th>
</tr>
</thead>
<tbody>
{% for note in notes %}
<tr {% if note.importance == 'L' %}class="info"{% endif %}{% if note.importance == 'M' %}class="warning"{% endif %}{% if note.importance == 'H' %}class="danger"{% endif %} data-id="{{ note.id }}">
<tr data-id="{{ note.id }}">
<!-- <td>{{ note.id }}</td> -->
<td>{{ note.issuee }}</td>
<td>{{ note.snippet }}</td>
<td>{{ note.importance_display }}</td>
<td>
<span class="label label-{% if note.importance == 'L' %}info{% elif note.importance == 'M' %}warning{% elif note.importance == 'H' %}danger{% endif %}">
{{ note.importance_display }}
</span>
</td>
<td>{{ note.issuer }}</td>
<td>
{% for attachment in note.attachments %}
<a href="{% url 'attachment' attachment.id %}">{{ attachment.file_name }}</a><br/>
{% endfor %}
</td>
<td>{{ note.date }}</td>
</tr>
{% endfor %}

View File

@ -6,21 +6,17 @@
<h2 class="sub-header">New Note</h2>
<div class="row">
<div class="col-xs-18 col-md-12">
<form action="" method="post">{% autoescape off %}{% get_csrf_html request %}{% endautoescape %}
<p><label for="">Player Filter:</label> <input id="id_filter" type="text"/></p>
<form action="" method="post" enctype="multipart/form-data">{% autoescape off %}{% get_csrf_html request %}{% endautoescape %}
<p><label for="id_filter">Player Filter:</label> <input id="id_filter" type="text"/></p>
{{ form }}
<p>
<label for="attachments">Attachments:</label>
<input id="attachments" name="attachments" type="file" multiple/>
</p>
<button id="saveButton" class="btn btn-primary" type="submit">Save</button>
</form>
</div>
</div>
<div class="row">
<div class="col-xs-6 col-md-4">
</div>
<div class="col-xs-12 col-md-8">
</div>
</div>
</div>
<script>
$(document).ready(function() {

View File

@ -15,7 +15,7 @@
<div class="col-xs-12 col-md-8">
<form action="" method="post">{% autoescape off %}{% get_csrf_html request %}{% endautoescape %}
<p>Issuer: {{ note.staff.username }} </p>
<p>Importance:
<p><label for="noteImportance">Importance:</label>
{% if user.is_staff or user == note.staff %}
<select id="noteImportance" name="importance">
{% for p in form.importance %}
@ -32,23 +32,44 @@
</form>
</div>
</div>
{% if note.attachments.all|length > 0 %}
<div class="row">
<div class="col-xs-6 col-md-4">
</div>
<div class="col-xs-12 col-md-8">
</div>
<div class="col-xs-9 col-md-6">
<table>
<thead>
<tr><th>Attachments</th></tr>
</thead>
<tbody>
{% for attachment in note.attachments %}
<tr>
<td>
<a href="{% url 'attachment' attachment.id %}">{{ attachment.file_name }}</a>
<span class="delete-attachment" data-name="{{ attachment.file_name }}" data-id="{{ attachment.id }}">
<i class="glyphicon glyphicon-remove-circle text-danger"></i>
</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
<br/>
<button type="button" class="btn btn-sm btn-primary add-attachment" data-model="N" data-ref="{{ note.id }}">Add Attachment(s)</button>
</div>
<script>
$("#saveButton").hide();
$("#noteImportance").change(function() {
if (("{{ user.username }}" == "{{ note.staff.username }}" || "{{ user.is_staff }}" == "True") && $(this).val() != "{{ note.importance }}") {
if (("{{ user.username }}" === "{{ note.staff.username }}" || "{{ user.is_staff }}" === "True") && $(this).val() !== "{{ note.importance }}") {
$("#saveButton").show();
} else {
$("#saveButton").hide();
}
});
</script>
{% include 'minecraft_manager/modal/add_attachment.html' %}
{% include 'minecraft_manager/modal/delete_attachment.html' %}
{% endblock section %}

View File

@ -2,7 +2,7 @@
{% block title %}Players{% endblock %}
{% block section %}
<div id="content" hidden="hidden">
<table id="model-table" class="table table-hover link-table">
<table id="model-table" class="table table-striped table-hover link-table">
<thead>
<tr>
<th>Username</th>
@ -13,10 +13,18 @@
</thead>
<tbody>
{% for player in players %}
<tr {% if player.uuid in bans %}class="danger"{% endif %}{% if player.uuid not in bans %}class="success"{% endif %} data-id="{{ player.id }}">
<tr data-id="{{ player.id }}">
<td>{{ player.username }}{% if user.usersettings.search_player_ip is True %}<span hidden="hidden">{{ player.ips }}</span>{% endif %}</td>
<td>{{ player.uuid }}</td>
<td>{% if player.uuid in bans %}True {% else %}False{% endif %}</td>
<td>
{% if player.uuid in bans %}
Yes
<span><i class="glyphicon glyphicon-remove-circle text-danger"></i></span>
{% else %}
No
<span><i class="glyphicon glyphicon-ok-circle text-success"></i></span>
{% endif %}
</td>
<td>{{ player.last_seen }}</td>
</tr>
{% endfor %}

View File

@ -6,12 +6,13 @@
<h2 class="sub-header">Player Info ({{ player.username }})</h2>
<div class="row">
<div class="col-xs-9 col-md-6">
<p>UUID: {{ player.uuid }}</p>
<p>UUID: <code>{{ player.uuid }}</code> <button data-copy="{{ player.uuid }}" class="btn btn-xs btn-primary"><i class="glyphicon glyphicon-copy"></i></button></p>
<p>Discord ID: {% if player.discord_id %}<code>{{ player.discord_id }}</code> <button data-copy="{{ player.discord_id }}" class="btn btn-xs btn-primary"><i class="glyphicon glyphicon-copy"></i></button>{% else %}N/A{% endif %}</p>
{% if player.auth_user %}
<p>Connected User: {{ player.auth_user.username }}</p>
{% endif %}
{% if player.application %}
<p>Application: <a href="{% url "application" %}{{ player.application.id }}">Here</a></p>
<p>Application: <a href="{% url "application" %}{{ player.application.id }}">{{ player.application.username }}</a></p>
{% endif %}
<p>Donor Status: {{ player.donor_status }}</p>
<p>First Seen: {{ player.first_seen }}</p>
@ -20,14 +21,35 @@
</div>
<div class="col-xs-9 col-md-6">
<h4>Tickets</h4>
<table class="table table-hover link-table">
<table class="table table-striped table-hover link-table">
<thead>
<tr>
<td>ID</td>
<td>Message</td>
<td>Resolved</td>
<td>Attachments</td>
</tr>
</thead>
<tbody>
{% if form.tickets %}
{% for ticket in form.tickets %}
<tr {% if ticket.resolved is True %}class="success"{% else %}class="danger"{% endif %} data-id="{{ ticket.id }}" data-url="{% url "ticket" %}">
<tr data-id="{{ ticket.id }}" data-url="{% url "ticket" %}">
<td>{{ ticket.id }}</td>
<td>{{ ticket.snippet }}</td>
<td>{{ ticket.resolved }}</td>
<td>
{% if ticket.resolved is True %}
<span><i class="glyphicon glyphicon-ok-circle text-success"></i></span>
{% else %}
<span><i class="glyphicon glyphicon-remove-circle text-danger"></i></span>
{% endif %}
</td>
<td>
{% for note in ticket.notes %}
{% for attachment in note.attachments %}
<a href="{% url 'attachment' attachment.id %}">{{ attachment.file_name }}</a> ({{ note.author.username }})<br/>
{% endfor %}
{% endfor %}
</td>
</tr>
{% endfor %}
{% else %}
@ -36,15 +58,33 @@
</tbody>
</table>
<br/>
<table class="table table-hover link-table">
<h4>Notes</h4>
<h4>Notes</h4>
<table class="table table-striped table-hover link-table">
<thead>
<tr>
<td>Message</td>
<td>Importance</td>
<td>Attachments</td>
</tr>
</thead>
<tbody>
{% if form.notes %}
{% for note in form.notes %}
<tr {% if note.importance == 'L' %}class="info"{% endif %}{% if note.importance == 'M' %}class="warning"{% endif %}{% if note.importance == 'H' %}class="danger"{% endif %} data-id="{{ note.id }}" data-url="{% url "note" %}">
<tr data-id="{{ note.id }}" data-url="{% url "note" %}">
<!-- {{ note.id }} -->
<td>{{ note.snippet }}</td>
<td>{{ note.importance_display }}</td>
<td>
<span class="label label-{% if note.importance == 'L' %}info{% elif note.importance == 'M' %}warning{% elif note.importance == 'H' %}danger{% endif %}">
{{ note.importance_display }}
</span>
</td>
<td>
{% for attachment in note.attachments %}
<a href="{% url 'attachment' attachment.id %}">{{ attachment.file_name }}</a>
{% if note.staff %}({{ note.staff.username }}){% endif %}
<br/>
{% endfor %}
</td>
</tr>
{% endfor %}
{% else %}
@ -54,14 +94,22 @@
</table>
<a class="btn btn-primary" href="{% url 'note_add' %}?player={{ player.id }}">Add Note</a>
<br/><br/>
<h4>IPs</h4>
<table class="table table-striped table-hover link-table">
<h4>IPs</h4>
<thead>
<tr>
<th>IP</th>
<th>Last Used</th>
<th>Associated Users</th>
</tr>
</thead>
<tbody>
{% if form.ips %}
{% for ip in form.ips %}
<tr class="default" data-id="" data-url="{% url 'ip' ip.id %}">
<!-- {{ ip.id }} -->
<td>{{ ip.ip }} ({{ ip.last_used_formatted }})</td>
<td>{{ ip.ip }}</td>
<td>{{ ip.last_used_formatted }}</td>
{% if ip.associated %}
<td>
{% for assoc in ip.associated %}
@ -81,4 +129,21 @@
</div>
</div>
</div>
<script>
document.querySelectorAll('[data-copy]').forEach((elem) => {
elem.addEventListener('click', () => {
const text = document.createElement('textarea');
text.value = elem.dataset.copy;
text.style.top = "0";
text.style.left = "0";
text.style.position = "fixed";
document.body.appendChild(text);
text.focus();
text.select();
document.execCommand('copy');
text.remove();
});
});
</script>
{% endblock section %}

View File

@ -2,13 +2,14 @@
{% block title %}Tickets{% endblock %}
{% block section %}
<div id="content" hidden="hidden">
<table id="model-table" class="table table-hover link-table">
<table id="model-table" class="table table-striped table-hover link-table">
<thead>
<tr>
<th>ID</th>
<th>Player</th>
<th>Message</th>
<th>Priority</th>
<th>Attachments</th>
<th>Claimed</th>
<th>Resolved</th>
<th>Date</th>
@ -16,13 +17,30 @@
</thead>
<tbody>
{% for ticket in tickets %}
<tr {% if ticket.resolved is True %}class="info"{% elif ticket.priority == 'L' %}class="success"{% elif ticket.priority == 'M' %}class="warning"{% elif ticket.priority == 'H' %}class="danger"{% endif %} data-id="{{ ticket.id }}">
<tr data-id="{{ ticket.id }}">
<td>{{ ticket.id }}</td>
<td>{{ ticket.issuer }}</td>
<td>{{ ticket.snippet }}</td>
<td>{{ ticket.priority_display }}</td>
<td>
<span class="label label-{% if ticket.priority == 'L' %}info{% elif ticket.priority == 'M' %}warning{% elif ticket.priority == 'H' %}danger{% endif %}">
{{ ticket.priority_display }}
</span>
</td>
<td>
{% for note in ticket.notes %}
{% for attachment in note.attachments %}
<a href="{% url 'attachment' attachment.id %}">{{ attachment.file_name }}</a><br/>
{% endfor %}
{% endfor %}
</td>
<td>{{ ticket.claimed_by }}</td>
<td>{{ ticket.resolved }}</td>
<td>
{% if ticket.resolved %}
<span><i class="glyphicon glyphicon-ok-circle text-success"></i></span>
{% else %}
<span><i class="glyphicon glyphicon-remove-circle text-danger"></i></span>
{% endif %}
</td>
<td>{{ ticket.date }}</td>
</tr>
{% endfor %}

View File

@ -74,20 +74,42 @@
{% endif %}
<p>Created: {{ ticket_note.date }}</p>
<p>Last Update: {{ ticket_note.last_update }}</p>
{% if ticket_note.attachments|length > 0 %}
<table>
<thead>
<tr><th>Attachments</th></tr>
</thead>
<tbody>
{% for attachment in ticket_note.attachments %}
<tr>
<td>
<a href="{% url 'attachment' attachment.id %}">{{ attachment.file_name }}</a>
<span class="delete-attachment" data-name="{{ attachment.file_name }}" data-id="{{ attachment.id }}">
<i class="glyphicon glyphicon-remove-circle text-danger"></i>
</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</div>
{% endfor %}
</div>
{% if not form.has_ticket_note and not form.show_ticket_note %}
<div id="createDiv" class="row">
<button class="btn btn-primary" onClick="showNote()">Create Note</button>
<button class="btn btn-primary" onClick="showNote();">Create Note</button>
</div>
{% endif %}
<div id="ticket_noteDiv" class="row" {% if not form.show_ticket_note %}style="display: none;"{% endif %}>
<br/>
<h3>Note</h3>
<form action="" method="POST">{% autoescape off %}{% get_csrf_html request %}{% endautoescape %}
<form action="" method="POST" enctype="multipart/form-data">{% autoescape off %}{% get_csrf_html request %}{% endautoescape %}
{{ form.ticket_note_form }}
<br/>
<p>
<label for="attachments">Attachments:</label>
<input id="attachments" name="attachments" type="file" multiple/>
</p>
<button type="submit" class="btn btn-primary" name="ticket_note" value="{% if form.has_ticket_note %}edit{% else %}create{% endif %}">Save</button>
</form>
</div>
@ -97,7 +119,7 @@
$("#saveButton").hide();
$("#ticketStaff").change(function() {
var $priority = $("#ticketPriority");
if ($(this).val() != "{{ ticket.staff.id }}" || $priority.val() != "{{ ticket.priority }}") {
if ($(this).val() !== "{{ ticket.staff.id }}" || $priority.val() !== "{{ ticket.priority }}") {
toggleSave(true);
} else {
toggleSave(false);
@ -105,7 +127,7 @@
});
$("#ticketPriority").change(function() {
var $staff = $("#ticketStaff");
if ($(this).val() != "{{ ticket.priority }}" || $staff.val() != "{{ ticket.staff.id }}") {
if ($(this).val() !== "{{ ticket.priority }}" || $staff.val() !== "{{ ticket.staff.id }}") {
toggleSave(true);
} else {
toggleSave(false);
@ -113,10 +135,10 @@
});
function toggleSave(toggle) {
if (toggle == true) {
if (toggle === true) {
$("#resolveButton").hide();
$("#saveButton").show();
} else if (toggle == false) {
} else if (toggle === false) {
$("#resolveButton").show();
$("#saveButton").hide();
}
@ -128,4 +150,5 @@
$("#editBtn").hide();
}
</script>
{% include 'minecraft_manager/modal/delete_attachment.html' %}
{% endblock section %}

View File

@ -33,6 +33,5 @@ def get_sidebar(current_app, request):
show_chat = True if getattr(settings, 'GLOBAL_LOG', None) is not None else False
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'))
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'))
return ret

View File

@ -0,0 +1,10 @@
from django import template
register = template.Library()
def has_prefix(value: str, arg: str) -> bool:
return value.startswith(arg)
register.filter('has_prefix', has_prefix)

106
urls.py
View File

@ -1,71 +1,47 @@
from django.conf.urls import url
from django.urls import path
from django.views.generic import RedirectView
from django.contrib.auth.decorators import login_required, permission_required
import minecraft_manager.views as mcm
urlpatterns = [
url(r'^$', RedirectView.as_view(pattern_name='overview')),
#Dashboard
url(r'^dashboard/overview/$', login_required(mcm.Overview.as_view()), name="overview"),
url(r'^dashboard/ban/$', login_required(mcm.Ban.as_view()), name="ban"),
#Alerts
url(r'^dashboard/alert/$', login_required(mcm.Alert.as_view()), name="alert"),
url(r'^dashboard/alert/(?P<alert_id>[0-9]{1,5})/$', login_required(mcm.AlertInfo.as_view())),
#Applications
url(r'^dashboard/application/$', login_required(mcm.Application.as_view()), name="application"),
url(r'^dashboard/reference/$', login_required(mcm.Reference.as_view()), name="reference"),
url(r'^dashboard/application/(?P<application_id>[0-9]{1,5})/$', login_required(mcm.ApplicationInfo.as_view())),
#Players
url(r'^dashboard/player/$', login_required(mcm.Player.as_view()), name="player"),
url(r'^dashboard/player/(?P<player_id>[0-9]{1,5})/$', login_required(mcm.PlayerInfo.as_view())),
#Tickets
url(r'^dashboard/ticket/$', login_required(mcm.Ticket.as_view()), name="ticket"),
url(r'^dashboard/ticket/(?P<ticket_id>[0-9]{1,5})/$', login_required(mcm.TicketInfo.as_view())),
#Warnings
url(r'^dashboard/note/$', login_required(mcm.Note.as_view()), name="note"),
url(r'^dashboard/note/(?P<note_id>[0-9]{1,5})/$', login_required(mcm.NoteInfo.as_view())),
url(r'^dashboard/note/add$', login_required(mcm.NoteAdd.as_view()), name="note_add"),
#IP
url(r'^dashboard/ip/(?P<ip_id>[0-9]{1,5})/$', login_required(mcm.IP.as_view()), name="ip"),
#Report
url(r'^report/$', login_required(mcm.Report.as_view()), name="report"),
#Chat
url(r'^dashboard/chat/$', permission_required('minecraft_manager.chat')(mcm.Chat.as_view()), name="chat"),
#Bots
url(r'^dashboard/bots/$', permission_required('minecraft_manager.bots')(mcm.Bots.as_view()), name="bots"),
path('', RedirectView.as_view(pattern_name='overview')),
# Dashboard
path('overview/', login_required(mcm.Overview.as_view()), name="overview"),
path('ban/', login_required(mcm.Ban.as_view()), name="ban"),
# Alerts
path('alert/', login_required(mcm.Alert.as_view()), name="alert"),
path('alert/<int:alert_id>/', login_required(mcm.AlertInfo.as_view()), name="alert_info"),
# Applications
path('application/', login_required(mcm.Application.as_view()), name="application"),
path('reference/', login_required(mcm.Reference.as_view()), name="reference"),
path('application/<int:application_id>/', login_required(mcm.ApplicationInfo.as_view()), name="application_info"),
# Players
path('player/', login_required(mcm.Player.as_view()), name="player"),
path('player/<int:player_id>/', login_required(mcm.PlayerInfo.as_view()), name="player_info"),
# Tickets
path('ticket/', login_required(mcm.Ticket.as_view()), name="ticket"),
path('ticket/<int:ticket_id>/', login_required(mcm.TicketInfo.as_view()), name="ticket_info"),
# Notes
path('note/', login_required(mcm.Note.as_view()), name="note"),
path('note/<int:note_id>/', login_required(mcm.NoteInfo.as_view()), name='note_info'),
path('note/add', login_required(mcm.NoteAdd.as_view()), name="note_add"),
# Attachments
path('attachment/<int:attachment_id>/', login_required(mcm.Attachment.as_view()), name="attachment"),
path('attachment/<str:ref_model>/<int:ref_id>/', login_required(mcm.AddAttachment.as_view()), name='attachment_add'),
# IP
path('ip/<int:ip_id>/', login_required(mcm.IP.as_view()), name="ip"),
# Report
path('report/', login_required(mcm.Report.as_view()), name="report"),
# Chat
path('chat/', permission_required('minecraft_manager.chat')(mcm.Chat.as_view()), name="chat"),
]
# Possible future feature
# from django.conf import settings
#
# LOGIN_REQUIRED = settings.LOGIN_REQUIRED if hasattr(settings, 'LOGIN_REQUIRED') else False
#
# urlpatterns = [
# #Dashboard
# url(r'^dashboard/overview/$', login_required(mcm.Overview.as_view()) if LOGIN_REQUIRED else mcm.Overview.as_view(), name="overview"),
# url(r'^dashboard/coreprotect/$', login_required(mcm.CoreProtect.as_view()) if LOGIN_REQUIRED else mcm.CoreProtect.as_view(), name="coreprotect"),
# url(r'^dashboard/activity/$', login_required(mcm.Activity.as_view()) if LOGIN_REQUIRED else mcm.Activity.as_view(), name="activity"),
# url(r'^dashboard/ban/$', login_required(mcm.Ban.as_view()) if LOGIN_REQUIRED else mcm.Ban.as_view(), name="ban"),
# #Alerts
# url(r'^dashboard/alert/$', login_required(mcm.Alert.as_view()) if LOGIN_REQUIRED else mcm.Alert.as_view(), name="alert"),
# url(r'^dashboard/alert/(?P<alert_id>[0-9]{1,5})/$', login_required(mcm.AlertInfo.as_view()) if LOGIN_REQUIRED else mcm.AlertInfo.as_view()),
# #Applications
# url(r'^dashboard/application/$', login_required(mcm.Application.as_view()) if LOGIN_REQUIRED else mcm.Application.as_view(), name="application"),
# url(r'^dashboard/application/(?P<application_id>[0-9]{1,5})/$', login_required(mcm.ApplicationInfo.as_view()) if LOGIN_REQUIRED else mcm.ApplicationInfo.as_view()),
# #Players
# url(r'^dashboard/player/$', login_required(mcm.Player.as_view()) if LOGIN_REQUIRED else mcm.Player.as_view(), name="player"),
# url(r'^dashboard/player/(?P<player_id>[0-9]{1,5})/$', login_required(mcm.PlayerInfo.as_view()) if LOGIN_REQUIRED else mcm.PlayerInfo.as_view()),
# #Tickets
# url(r'^dashboard/ticket/$', login_required(mcm.Ticket.as_view()) if LOGIN_REQUIRED else mcm.Ticket.as_view(), name="ticket"),
# url(r'^dashboard/ticket/(?P<ticket_id>[0-9]{1,5})/$', login_required(mcm.TicketInfo.as_view()) if LOGIN_REQUIRED else mcm.TicketInfo.as_view()),
# #Warnings
# url(r'^dashboard/warning/$', login_required(mcm.Warning.as_view()) if LOGIN_REQUIRED else mcm.Warning.as_view(), name="warning"),
# url(r'^dashboard/warning/(?P<warning_id>[0-9]{1,5})/$', login_required(mcm.WarningInfo.as_view()) if LOGIN_REQUIRED else mcm.WarningInfo.as_view()),
# url(r'^dashboard/warning/add$', login_required(mcm.WarningAdd.as_view()) if LOGIN_REQUIRED else mcm.WarningAdd.as_view(), name="warning_add"),
# #Chat
# url(r'^dashboard/chat/$', login_required(mcm.Chat.as_view()) if LOGIN_REQUIRED else mcm.Chat.as_view(), name="chat"),
# #Bots
# url(r'^dashboard/bots/$', login_required(mcm.Bots.as_view()) if LOGIN_REQUIRED else mcm.Bots.as_view(), name="bots"),
# ]

View File

@ -1,5 +1,9 @@
import discord, requests
import discord
import requests
from django.conf import settings
from django.shortcuts import reverse
from django.templatetags.static import static
from minecraft_manager.models import Player, Application
@ -26,67 +30,66 @@ def build_application(application):
embed = discord.Embed(colour=discord.Colour(0x417505))
embed.title = "Application"
embed.set_thumbnail(
url="https://minotar.net/helm/{0}/100.png".format(application.username))
url=f"https://minotar.net/helm/{application.username}/100.png")
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="Favorite Activity", value=application.player_type)
embed.add_field(name="Ever been banned", value=application.ever_banned)
if application.ever_banned:
if application.ever_banned and application.ever_banned_explanation:
embed.add_field(name="Reason for being banned", value=application.ever_banned_explanation)
embed.add_field(name="Reference", value=application.reference)
if application.reference:
embed.add_field(name="Referral", value=application.reference)
embed.add_field(name="Read the Rules", value=application.read_rules)
embed.add_field(name="Date", value=application.date_display)
embed.timestamp = application.date
embed.add_field(name="Status", value=application.status)
embed.add_field(name="Link", value="{}".format(url_path(settings.MCM_BASE_LINK, 'dashboard/application', application.id)))
embed.add_field(name="Link", value=full_reverse('application_info', application.id), inline=False)
return embed
def build_ticket(ticket, link):
embed = discord.Embed(colour=discord.Colour(0x417505))
embed.title = "Ticket"
embed.set_thumbnail(
url="https://cdn.discordapp.com/avatars/454457830918062081/b5792489bc43d9e17b8f657880a17dd4.png")
embed.add_field(name="Date", value=ticket.date_display)
embed.set_thumbnail(url=full_static("favicon.png"))
embed.timestamp = ticket.date
embed.add_field(name="Player", value=ticket.player.username.replace("_", "\\_"))
embed.add_field(name="Priority", value=ticket.priority_display)
if ticket.x and ticket.y and ticket.z and ticket.world:
embed.add_field(name="Location", value=ticket.location)
embed.add_field(name="Message", value=ticket.message)
embed.add_field(name="Link", value=link)
embed.add_field(name="Link", value=link, inline=False)
return embed
def build_warning(warning, link):
def build_note(note, link):
embed = discord.Embed(colour=discord.Colour(0x417505))
embed.title = "Warning"
embed.set_thumbnail(
url="https://cdn.discordapp.com/avatars/454457830918062081/b5792489bc43d9e17b8f657880a17dd4.png")
embed.add_field(name="Date", value=warning.date_display)
embed.add_field(name="Player", value=warning.player.username.replace("_", "\\_"))
embed.add_field(name="Importance", value=warning.importance_display)
embed.add_field(name="Message", value=warning.message)
embed.add_field(name="Link", value=link)
embed.title = "Note"
embed.set_thumbnail(url=full_static("favicon.png"))
embed.timestamp = note.date
embed.add_field(name="Player", value=note.player.username.replace("_", "\\_"))
embed.add_field(name="Importance", value=note.importance_display)
embed.add_field(name="Message", value=note.message)
embed.add_field(name="Link", value=link, inline=False)
return embed
def validate_username(username):
response = requests.get("https://api.mojang.com/users/profiles/minecraft/{}".format(username))
response = requests.get(f"https://api.mojang.com/users/profiles/minecraft/{username}")
if response.status_code == 200:
return True
return False
def url_path(*args):
value = []
for arg in args:
arg = str(arg)
if arg.startswith('/'):
arg = arg[1:]
if arg.endswith('/'):
arg = arg[:-1]
value.append(arg)
return '/'.join(value)
def full_reverse(viewname, *args):
base = settings.MCM_DOMAIN.rstrip('/')
view = reverse(viewname, args=args)
return f"{base}{view}"
def full_static(assetname):
base = settings.MCM_DOMAIN.rstrip('/')
asset = static(assetname)
return f"{base}{asset}"
class Captcha:
@ -121,4 +124,4 @@ class Captcha:
def add_error(self, error):
if error not in self.errors:
self.errors.append(error)
self.errors.append(error)

153
views.py
View File

@ -1,25 +1,20 @@
#create your views here.
# https://api.mojang.com/users/profiles/minecraft/<username>
from __future__ import absolute_import
import json, datetime, pytz, os, sys
import json, datetime, pytz, os
from django.utils import timezone
from itertools import chain
from django.http import JsonResponse
from django.http import JsonResponse, HttpResponse
from django.shortcuts import render, reverse, redirect
from django.views.decorators.csrf import csrf_protect
from django.utils.decorators import method_decorator
from django.conf import settings
from django.views.generic import View
from django.contrib.auth.models import User
from minecraft_manager.models import Application as AppModel, Player as PlayerModel, Ticket as TicketModel, TicketNote as TicketNoteModel, Note as NoteModel, IP as IPModel, Alert as AlertModel, UserSettings as UserSettingsModel
from minecraft_manager.models import Application as AppModel, Player as PlayerModel, Ticket as TicketModel, TicketNote as TicketNoteModel, Note as NoteModel, IP as IPModel, Alert as AlertModel, UserSettings as UserSettingsModel, Attachment as AttachmentModel, RefModels
from minecraft_manager.forms import TicketNoteForm, NoteForm
from minecraft_manager.overview import overview_data
from minecraft_manager.utils import resolve_player
from minecraft_manager.utils import resolve_player, build_note, full_reverse
import minecraft_manager.api.api as API
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):
@ -44,14 +39,12 @@ class Overview(View):
class CoreProtect(View):
def get(self, request):
#http://www.24carrotcraft.com/assets/cp/index.php?username=etzelia
return render(request, 'minecraft_manager/coreprotect.html', {'current_app': 'coreprotect'})
class Activity(View):
def get(self, request):
#http://www.24carrotcraft.com/assets/cp/activity.php?username=etzelia
return render(request, 'minecraft_manager/activity.html', {'current_app': 'activity'})
@ -141,7 +134,6 @@ class Application(View):
class Reference(View):
def get(self, request):
get = request.GET
applications = AppModel.objects.all()
return render(request, 'minecraft_manager/reference.html', {'current_app': 'application', 'applications': applications})
@ -163,7 +155,8 @@ class ApplicationInfo(View):
application.accepted = False
application.save()
API.plugin(post['accept'], application.username)
API.discord_mcm("Application #**{0}** was **{1}** by **{2}**".format(application.id, "Accepted" if application.accepted else "Denied", request.user.player.username))
link = full_reverse('application_info', application_id)
API.discord_mcm("[Application #**{0}**]({3}) was **{1}** by **{2}**".format(application.id, "Accepted" if application.accepted else "Denied", request.user.player.username, link))
return render(request, 'minecraft_manager/application_info.html',
{'current_app': 'application', 'application': application})
@ -192,7 +185,7 @@ class Player(View):
class PlayerInfo(View):
def get(self, request, player_id):
def _info(self, request, player_id):
player = PlayerModel.objects.get(id=player_id)
ips = IPModel.api.filter(player=player)
tickets = TicketModel.objects.filter(player=player)
@ -201,20 +194,16 @@ class PlayerInfo(View):
return render(request, 'minecraft_manager/player_info.html',
{'current_app': 'player', 'player': player, 'form': form})
def get(self, request, player_id):
return self._info(request, player_id)
def post(self, request, player_id):
player = PlayerModel.objects.get(id=player_id)
ips = IPModel.api.filter(player=player)
tickets = TicketModel.objects.filter(player=player)
notes = NoteModel.objects.filter(player=player)
form = {'ips': ips, 'tickets': tickets, 'notes': notes}
return render(request, 'minecraft_manager/player_info.html',
{'current_app': 'player', 'player': player, 'form': form})
return self._info(request, player_id)
class Ticket(View):
def get(self, request):
get = request.GET
tickets1 = TicketModel.objects.filter(resolved=False).order_by('-id')
tickets2 = TicketModel.objects.filter(resolved=True).order_by('-id')
tickets = list(chain(tickets1, tickets2))
@ -245,14 +234,17 @@ class TicketInfo(View):
def post(self, request, ticket_id):
post = request.POST
ticket = TicketModel.objects.get(id=ticket_id)
link = full_reverse('ticket_info', ticket_id)
if 'priority' in post:
if post['priority'] != ticket.priority:
API.discord_mcm(
"Ticket #**{0}**'s priority was changed from **{1}** to **{2}** by **{3}**".format(ticket.id,
"[Ticket #**{0}**]({4})'s priority was changed from **{1}** to **{2}** by **{3}**".format(ticket.id,
ticket.priority_display,
TicketModel.priority_code_to_display(
post['priority']),
request.user.username))
request.user.username,
link)
)
ticket.priority = post['priority']
if 'staff' in post and 'resolved' not in post:
if not ticket.staff or request.user.is_staff:
@ -260,13 +252,13 @@ class TicketInfo(View):
if post['staff'] != str(getattr(ticket.staff, 'id', '-1')):
if post['staff'] == str(request.user.id):
API.discord_mcm(
"Ticket #**{0}** was claimed by **{1}**".format(ticket.id, request.user.username))
"[Ticket #**{0}**]({2}) was claimed by **{1}**".format(ticket.id, request.user.username, link))
else:
API.discord_mcm(
"Ticket #**{0}** was given to **{1}** by **{2}**".format(ticket.id, staff.username, request.user.username))
"[Ticket #**{0}**]({3}) was given to **{1}** by **{2}**".format(ticket.id, staff.username, request.user.username, link))
ticket.staff = staff
if 'resolved' in post:
API.discord_mcm("Ticket #**{0}** was resolved by **{1}**".format(ticket.id, request.user.username))
API.discord_mcm("[Ticket #**{0}**]({2}) was resolved by **{1}**".format(ticket.id, request.user.username, link))
ticket.resolved = True
ticket.save()
@ -283,6 +275,11 @@ class TicketInfo(View):
db.message = n.message
db.last_update = timezone.now()
db.save()
# Refresh to get the ID for attachments
note = TicketNoteModel.objects.get(ticket=ticket, author=request.user)
for file in request.FILES.getlist('attachments', []):
attachment = AttachmentModel(ref_model=RefModels.TICKET_NOTE[0], ref_id=note.id, file=file)
attachment.save()
else:
show_ticket_note = True
else:
@ -317,23 +314,31 @@ class NoteInfo(View):
def get(self, request, note_id):
note = NoteModel.objects.get(id=note_id)
form = {'importance': NoteModel.IMPORTANCE}
return render(request, 'minecraft_manager/note_info.html', {'current_app': 'note', 'form': form, 'note': note})
return render(request, 'minecraft_manager/note_info.html', {
'current_app': 'note',
'form': form,
'note': note
})
def post(self, request, note_id):
post = request.POST
note = NoteModel.objects.get(id=note_id)
if 'importance' in post:
API.discord_mcm("Note #**{0}**'s importance was changed from {1} to {2} by {3}".format(note.id,
note.importance_display,
NoteModel.importance_code_to_display(
post[
'importance']),
request.user.player.username))
API.discord_mcm("[Note #**{0}**]({4})'s importance was changed from **{1}** to **{2}** by **{3}**".format(
note.id,
note.importance_display,
NoteModel.importance_code_to_display(post['importance']),
request.user.username,
full_reverse('note_info', note_id))
)
note.importance = post['importance']
note.save()
form = {'importance': NoteModel.IMPORTANCE}
return render(request, 'minecraft_manager/note_info.html',
{'current_app': 'note', 'form': form, 'note': note})
return render(request, 'minecraft_manager/note_info.html', context={
'current_app': 'note',
'form': form,
'note': note
})
class NoteAdd(View):
@ -344,8 +349,7 @@ class NoteAdd(View):
form = NoteForm()
if 'player' in get:
form.initial = {'player': get['player']}
return render(request, 'minecraft_manager/note_add.html',
{'current_app': 'note', 'form': form.as_p()})
return render(request, 'minecraft_manager/note_add.html', context={'current_app': 'note', 'form': form.as_p()})
def post(self, request):
post = request.POST
@ -354,15 +358,36 @@ class NoteAdd(View):
note = form.save()
note.staff = request.user
note.save()
API.discord_mcm(
"**{0}** made a **{1}** importance note for **{2}**\nPreview: {3}".format(note.staff.player.username,
note.importance_display,
note.player.username,
note.snippet))
return redirect("{0}{1}".format(reverse('note'), note.id))
API.discord_mcm(embed=build_note(note, full_reverse('note_info', note.id)))
for file in request.FILES.getlist('attachments', []):
attachment = AttachmentModel(ref_model=RefModels.NOTE[0], ref_id=note.id, file=file)
attachment.save()
return redirect("{0}{1}".format(full_reverse('note'), note.id))
else:
return render(request, 'minecraft_manager/note_add.html',
{'current_app': 'note', 'form': form})
return render(request, 'minecraft_manager/note_add.html', context={'current_app': 'note', 'form': form})
class Attachment(View):
def get(self, request, attachment_id):
attachment = AttachmentModel.objects.get(id=attachment_id)
resp = HttpResponse(attachment.file)
resp['Content-Disposition'] = f"attachment; filename={attachment.file_name}"
return resp
def delete(self, request, attachment_id):
attachment = AttachmentModel.objects.get(id=attachment_id)
attachment.delete()
return HttpResponse(status=204)
class AddAttachment(View):
def post(self, request, ref_model, ref_id):
for file in request.FILES.getlist('attachments', []):
attachment = AttachmentModel(ref_model=ref_model, ref_id=ref_id, file=file)
attachment.save()
return redirect(request.POST.get('next', reverse('overview')))
class IP(View):
@ -430,7 +455,6 @@ class Report(View):
class Chat(View):
@staticmethod
def replace_ascii(message):
return message.replace(" ", "\\040").replace("\"", "\\042").replace("#", "\\043").replace("$", "\\044")\
@ -449,38 +473,3 @@ class Chat(View):
else:
data = {'success': False, 'message': 'No chat type or message set.'}
return JsonResponse(data)
class Bots(View):
def get_bots(self):
bot_dir = getattr(settings, 'BOT_DIR', None)
bots = []
if bot_dir:
for file in os.listdir(bot_dir):
if file.endswith('.bot.py'):
ve = file.replace('.bot.py', '')
py = os.path.join(bot_dir, ve, 'bin/python')
if os.path.isfile(py):
bots.append(Bot(file.replace('.bot.py', ''), False, py))
else:
bots.append(Bot(file.replace('.bot.py', ''), False, sys.executable))
# Also get packaged MCM bots
if getattr(settings, 'DISCORD_BOT_TOKEN', None):
bots.append(Bot("Discord-MCM", True, None, discord_start, discord_stop, discord_restart, discord_status, discord_display))
return bots
def get(self, request):
return render(request, 'minecraft_manager/bots.html', {'current_app': 'bots', 'bots': self.get_bots()})
def post(self, request):
post = request.POST
for bot in self.get_bots():
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()})