From da447911867e14a05df53b1f403eef550a812903 Mon Sep 17 00:00:00 2001 From: Etzelia Date: Wed, 19 Sep 2018 21:56:17 -0500 Subject: [PATCH] Initial commit for Gitea --- .gitignore | 3 + __init__.py | 0 admin.py | 106 + api/__init__.py | 0 api/api.py | 142 + api/bot.py | 296 + api/urls.py | 12 + api/views.py | 423 + assets/bots/Discord.bot.py | 18 + assets/bots/OreAlert.bot.py | 16 + assets/coreprotect/blocks.txt | 9 + assets/migrate.sql | 55 + external/__init__.py | 0 external/stats.py | 85 + external/urls.py | 10 + external/views.py | 158 + forms.py | 53 + middleware.py | 16 + migrations/0001_initial.py | 128 + migrations/0002_auto_20180530_2223.py | 21 + migrations/0003_minecraftmanageruser.py | 29 + migrations/0004_auto_20180530_2241.py | 17 + migrations/0005_auto_20180601_2311.py | 20 + migrations/0006_auto_20180601_2312.py | 20 + migrations/0007_auto_20180724_2218.py | 18 + migrations/0008_auto_20180820_1406.py | 20 + migrations/__init__.py | 0 models.py | 438 + static/FilterByText.js | 25 + static/background.png | Bin 0 -> 821230 bytes static/bootstrap-dark.css | 7046 ++++++++++++++++ static/bootstrap-slate.css | 7276 +++++++++++++++++ static/bootstrap-solar.css | 6858 ++++++++++++++++ static/chart.min.js | 14 + static/dashboard-dark.css | 138 + static/dashboard-slate.css | 138 + static/dashboard-solar.css | 130 + static/dashboard.css | 127 + static/external.css | 70 + static/favicon.png | Bin 0 -> 2851 bytes static/jquery-3.3.1.min.js | 2 + static/load.svg | 1 + static/signin.css | 48 + static/world.png | Bin 0 -> 923 bytes static/world_go.png | Bin 0 -> 944 bytes templates/minecraft_manager/activity.html | 11 + templates/minecraft_manager/alert.html | 45 + templates/minecraft_manager/alert_info.html | 23 + templates/minecraft_manager/application.html | 46 + .../minecraft_manager/application_info.html | 43 + templates/minecraft_manager/ban.html | 45 + templates/minecraft_manager/base.html | 44 + templates/minecraft_manager/bots.html | 17 + templates/minecraft_manager/chat.html | 121 + templates/minecraft_manager/coreprotect.html | 11 + templates/minecraft_manager/dashboard.html | 214 + .../minecraft_manager/external/apply.html | 25 + .../minecraft_manager/external/base.html | 50 + templates/minecraft_manager/external/map.html | 26 + .../minecraft_manager/external/stats_all.html | 46 + .../external/stats_single.html | 44 + .../minecraft_manager/external/ticket.html | 13 + templates/minecraft_manager/logged_out.html | 9 + templates/minecraft_manager/login.html | 31 + templates/minecraft_manager/overview.html | 141 + templates/minecraft_manager/player.html | 40 + templates/minecraft_manager/player_info.html | 90 + templates/minecraft_manager/reference.html | 24 + templates/minecraft_manager/report.html | 84 + templates/minecraft_manager/ticket.html | 46 + templates/minecraft_manager/ticket_info.html | 129 + templates/minecraft_manager/warning.html | 46 + templates/minecraft_manager/warning_add.html | 29 + templates/minecraft_manager/warning_info.html | 52 + templatetags/__init__.py | 0 templatetags/csrf_html.py | 36 + templatetags/getattribute.py | 22 + templatetags/pretty_print.py | 23 + templatetags/sidebar.py | 69 + templatetags/template_settings.py | 10 + urls.py | 72 + utils.py | 103 + views.py | 497 ++ 83 files changed, 26363 insertions(+) create mode 100644 .gitignore create mode 100644 __init__.py create mode 100644 admin.py create mode 100644 api/__init__.py create mode 100644 api/api.py create mode 100644 api/bot.py create mode 100644 api/urls.py create mode 100644 api/views.py create mode 100644 assets/bots/Discord.bot.py create mode 100644 assets/bots/OreAlert.bot.py create mode 100644 assets/coreprotect/blocks.txt create mode 100644 assets/migrate.sql create mode 100644 external/__init__.py create mode 100644 external/stats.py create mode 100644 external/urls.py create mode 100644 external/views.py create mode 100644 forms.py create mode 100644 middleware.py create mode 100644 migrations/0001_initial.py create mode 100644 migrations/0002_auto_20180530_2223.py create mode 100644 migrations/0003_minecraftmanageruser.py create mode 100644 migrations/0004_auto_20180530_2241.py create mode 100644 migrations/0005_auto_20180601_2311.py create mode 100644 migrations/0006_auto_20180601_2312.py create mode 100644 migrations/0007_auto_20180724_2218.py create mode 100644 migrations/0008_auto_20180820_1406.py create mode 100644 migrations/__init__.py create mode 100644 models.py create mode 100644 static/FilterByText.js create mode 100644 static/background.png create mode 100644 static/bootstrap-dark.css create mode 100644 static/bootstrap-slate.css create mode 100644 static/bootstrap-solar.css create mode 100644 static/chart.min.js create mode 100644 static/dashboard-dark.css create mode 100644 static/dashboard-slate.css create mode 100644 static/dashboard-solar.css create mode 100644 static/dashboard.css create mode 100644 static/external.css create mode 100644 static/favicon.png create mode 100644 static/jquery-3.3.1.min.js create mode 100644 static/load.svg create mode 100644 static/signin.css create mode 100644 static/world.png create mode 100644 static/world_go.png create mode 100644 templates/minecraft_manager/activity.html create mode 100644 templates/minecraft_manager/alert.html create mode 100644 templates/minecraft_manager/alert_info.html create mode 100644 templates/minecraft_manager/application.html create mode 100644 templates/minecraft_manager/application_info.html create mode 100644 templates/minecraft_manager/ban.html create mode 100644 templates/minecraft_manager/base.html create mode 100644 templates/minecraft_manager/bots.html create mode 100644 templates/minecraft_manager/chat.html create mode 100644 templates/minecraft_manager/coreprotect.html create mode 100644 templates/minecraft_manager/dashboard.html create mode 100644 templates/minecraft_manager/external/apply.html create mode 100644 templates/minecraft_manager/external/base.html create mode 100644 templates/minecraft_manager/external/map.html create mode 100644 templates/minecraft_manager/external/stats_all.html create mode 100644 templates/minecraft_manager/external/stats_single.html create mode 100644 templates/minecraft_manager/external/ticket.html create mode 100644 templates/minecraft_manager/logged_out.html create mode 100644 templates/minecraft_manager/login.html create mode 100644 templates/minecraft_manager/overview.html create mode 100644 templates/minecraft_manager/player.html create mode 100644 templates/minecraft_manager/player_info.html create mode 100644 templates/minecraft_manager/reference.html create mode 100644 templates/minecraft_manager/report.html create mode 100644 templates/minecraft_manager/ticket.html create mode 100644 templates/minecraft_manager/ticket_info.html create mode 100644 templates/minecraft_manager/warning.html create mode 100644 templates/minecraft_manager/warning_add.html create mode 100644 templates/minecraft_manager/warning_info.html create mode 100644 templatetags/__init__.py create mode 100644 templatetags/csrf_html.py create mode 100644 templatetags/getattribute.py create mode 100644 templatetags/pretty_print.py create mode 100644 templatetags/sidebar.py create mode 100644 templatetags/template_settings.py create mode 100644 urls.py create mode 100644 utils.py create mode 100644 views.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..490fac0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.pyc +__pycache__/* +docs/build/* \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/admin.py b/admin.py new file mode 100644 index 0000000..abb8d91 --- /dev/null +++ b/admin.py @@ -0,0 +1,106 @@ +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, Warning, Ticket, Player, IP, UserSettings, Alert, Note + + +class PlayerInline(admin.StackedInline): + model = Player + can_delete = False + verbose_name_plural = 'player' + + +class UserAdmin(BaseUserAdmin): + inlines = (PlayerInline, ) + actions = ['deactivate_account'] + + def deactivate_account(self, request, queryset): + rows_updated = queryset.update(is_active=False) + if rows_updated == 1: + message_bit = "1 user was" + else: + message_bit = "%s users were" % rows_updated + self.message_user(request, "%s successfully deactivated." % message_bit) + deactivate_account.short_description = "Deactivate selected accounts" + + + + +class PlayerAdmin(admin.ModelAdmin): + search_fields = ["username", "uuid"] + + +class ApplicationAdmin(admin.ModelAdmin): + search_fields = ["username"] + #date_hierarchy = 'date' + + +class TicketPriorityFilter(admin.SimpleListFilter): + title = _('Priority') + parameter_name = 'priority' + + def lookups(self, request, model_admin): + return ( + ('0', _('Low')), + ('1', _('Medium')), + ('2', _('High')) + ) + + def queryset(self, request, queryset): + if self.value() == '0': + return queryset.filter(priority='L') + if self.value() == '1': + return queryset.filter(priority='M') + if self.value() == '2': + return queryset.filter(priority='H') + + +class TicketAdmin(admin.ModelAdmin): + search_fields = ["player__username", "staff__username"] + #date_hierarchy = 'date' + list_filter = (TicketPriorityFilter,) + + +class WarningSeverityFilter(admin.SimpleListFilter): + title = _('Severity') + parameter_name = 'severity' + + def lookups(self, request, model_admin): + return ( + ('0', _('Low')), + ('1', _('Medium')), + ('2', _('High')) + ) + + def queryset(self, request, queryset): + if self.value() == '0': + return queryset.filter(priority='L') + if self.value() == '1': + return queryset.filter(priority='M') + if self.value() == '2': + return queryset.filter(priority='H') + + +class WarningAdmin(admin.ModelAdmin): + search_fields = ["player__username", "staff__username"] + #date_hierarchy = 'date' + list_filter = (WarningSeverityFilter,) + + +try: + admin.site.unregister(User) + admin.site.register(User, UserAdmin) + admin.site.register(UserSettings) + admin.site.register(Application, ApplicationAdmin) + admin.site.register(Warning, WarningAdmin) + admin.site.register(Ticket, TicketAdmin) + admin.site.register(Player, PlayerAdmin) + admin.site.register(IP) + admin.site.register(Alert) + admin.site.register(Note) +except admin.sites.AlreadyRegistered: + pass + diff --git a/api/__init__.py b/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/api.py b/api/api.py new file mode 100644 index 0000000..8127661 --- /dev/null +++ b/api/api.py @@ -0,0 +1,142 @@ +import socket, requests, logging, os, datetime, pytz, mcstatus, discord +from minecraft_manager.models import Alert +from django.contrib.auth.models import User +from django.conf import settings + +logger = logging.getLogger(__name__) + + +def create_alert(message): + users = User.objects.filter(is_active=True) + for user in users: + alert = Alert(user=user, message=message) + alert.save() + + +PLUGIN_APPLICATION = 'application' +PLUGIN_ACCEPT = 'accept' +PLUGIN_DENY = 'deny' +PLUGIN_GLOBAL_CHAT = 'global' +PLUGIN_STAFF_CHAT = 'staff' + + +def plugin(key, command): + host = '127.0.0.1' + port = getattr(settings, 'PLUGIN_PORT', None) + full_command = "{0} {1}".format(key, command) + if port and plugin_exists(): + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect((host, port)) + sock.sendall(full_command.encode('utf-8')) + sock.close() + + +def discord_mcm(message='', embeds=None, ping=False): + discord_mcm_webhook = getattr(settings, 'DISCORD_MCM_WEBHOOK', None) + if discord_mcm_webhook: + 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 + + +def discord_notification(message='', embeds=None, ping=False): + discord_notification_webhook = getattr(settings, 'DISCORD_NOTIFICATION_WEBHOOK', None) + if discord_notification_webhook: + 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 + + +def discord_webhook(webhook_name=None, message=None, embed=None, ping=False): + webhook = getattr(settings, 'DISCORD_{}_WEBHOOK'.format(webhook_name.upper()), getattr(settings, 'DISCORD_WEBHOOK', None)) + if webhook and (message or embed): + 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 + return requests.post(webhook, json=data) + return None + + + +def strip_format(message): + return message.replace("§0", "").replace("§1", "").replace("§2", "").replace("§3", "").replace("§4", "") \ + .replace("§5", "").replace("§6", "").replace("§7", "").replace("§8", "").replace("§9", "").replace("§a", "") \ + .replace("§b", "").replace("§c", "").replace("§d", "").replace("§e", "").replace("§f", "").replace("§r", "") \ + .replace("§k", "").replace("§l", "").replace("§m", "").replace("§n", "").replace("§o", "") + + +def plugin_exists(): + return os.path.exists(os.path.join(getattr(settings, 'MINECRAFT_BASE_DIR', ''), 'plugins/MinecraftManager')) + + +def get_chats(as_timezone): + global_log = getattr(settings, 'GLOBAL_LOG', "") + staff_log = getattr(settings, 'STAFF_LOG', "") + log_time_format = getattr(settings, 'LOG_TIME_FORMAT', '%m/%d/%y %I:%M %p') + if plugin_exists(): + try: + with open(global_log, encoding='utf-8') as g: + g_chat = g.readlines()[-100:] + except OSError: + g_chat = [] + for idx, gc in enumerate(g_chat): + try: + date_str = gc.split("] ")[0].replace("[", "") + msg = gc.split("] ", 1)[1] + dt = datetime.datetime.strptime(date_str, log_time_format) + dt = pytz.timezone('US/Central').localize(dt) + dt = dt.astimezone(pytz.timezone(as_timezone)) + g_chat[idx] = {"date": dt.strftime(log_time_format), "text": msg} + except: + g_chat[idx] = {"date": "01/01/17 00:00 AM", "name": "Error", "text": "Parsing Line"} + try: + with open(staff_log, encoding='utf-8') as s: + s_chat = s.readlines()[-100:] + except OSError: + s_chat = [] + for idx, sc in enumerate(s_chat): + try: + date_str = sc.split("] ")[0].replace("[", "") + msg = sc.split("] ", 1)[1] + dt = datetime.datetime.strptime(date_str, log_time_format) + dt = pytz.timezone('US/Central').localize(dt) + dt = dt.astimezone(pytz.timezone(as_timezone)) + s_chat[idx] = {"date": dt.strftime(log_time_format), "text": msg} + except: + s_chat[idx] = {"date": "01/01/17 00:00 AM", "name": "Error", "text": "Parsing Line"} + + return {'global': g_chat, 'staff': s_chat} + return None + + +def get_query(): + try: + server = mcstatus.MinecraftServer.lookup(getattr(settings, 'SERVER_QUERY_IP', None)) + query = server.query() + return {'max': str(query.players.max), 'online': str(query.players.online), + 'players': sorted(query.players.names)} + except: + return {'max': 0, 'online': 0, + 'players': []} \ No newline at end of file diff --git a/api/bot.py b/api/bot.py new file mode 100644 index 0000000..942bbc3 --- /dev/null +++ b/api/bot.py @@ -0,0 +1,296 @@ +import discord, logging, re, sys, traceback, asyncio, datetime, os, time +from minecraft_manager.models import Application, Player +from minecraft_manager.api import api +from django.conf import settings +from django.db import close_old_connections +from threading import Thread + + +logger = logging.getLogger(__name__) + + +class Discord(discord.Client): + discord_game = 'MCM' + prefix = getattr(settings, 'DISCORD_BOT_PREFIX', '!') + auth_roles = getattr(settings, 'DISCORD_BOT_ROLES', []) + error_users = getattr(settings, 'DISCORD_ERROR_USERS', []) + token = None + + def __init__(self, token, **kwargs): + super().__init__(**kwargs) + self.token = token + + @asyncio.coroutine + def on_ready(self): + print('Logged in as') + print(self.user.name) + print(self.user.id) + print(discord.__version__) + print('Voice Loaded: {0}'.format(discord.opus.is_loaded())) + print('OAuth URL: https://discordapp.com/api/oauth2/authorize?client_id={0}&permissions=0&scope=bot'.format(self.user.id)) + print('------') + logger.info('Logged in as {0} ({1}) with discord.py v{2}'.format(self.user.name, self.user.id, discord.__version__)) + yield from self.change_presence(game=discord.Game(name=self.discord_game)) + + @asyncio.coroutine + def discord_message(self, channel, message): + if isinstance(message, discord.Embed): + for idx, field in enumerate(message.fields): + if not field.value: + message.set_field_at(idx, name=field.name, value="N/A") + yield from self.send_message(channel, embed=message) + else: + yield from self.send_message(channel, message) + + @asyncio.coroutine + def on_message(self, message): + + # IGNORE DM AND BOTS + if message.author.bot is True or message.channel.is_private is True: + return + + member_roles = [role.id for role in message.author.roles] + + # IF MEMBER IS NOT AUTHORIZED, IGNORE + if not any(role in self.auth_roles for role in member_roles): + return + + # FIX STALE DB CONNECTIONS + close_old_connections() + + # HELP + match = re.match("[{0}]help$".format(self.prefix), message.content) + if match: + embed = discord.Embed(colour=discord.Colour(0x417505)) + embed.set_thumbnail(url="https://cdn.discordapp.com/avatars/454457830918062081/b5792489bc43d9e17b8f657880a17dd4.png") + embed.add_field(name="Minecraft Manager Help", value="-----------------------------") + embed.add_field(name="{}[app ]search ".format(self.prefix), value="Search for applications by partial or exact username.") + embed.add_field(name="{}[app ]info ".format(self.prefix), value="Get detailed information about a specific application.") + embed.add_field(name="{}[app ]accept|deny ".format(self.prefix), value="Take action on an application.") + embed.add_field(name="{}compare".format(self.prefix), value="Compare Discord users to the Whitelist") + yield from self.discord_message(message.channel, embed) + # APP COMMANDS WITH APP ID + match = re.match("[{0}](?:app )?(i|info|a|accept|d|deny) (\d+)$".format(self.prefix), message.content) + if match: + if match.group(1) and match.group(2): + action = match.group(1)[:1] + action_display = "accept" if action == "a" else "deny" if action == "d" else "" + yield from self.discord_message(message.channel, "{0} App ID **{1}**".format( + "Retrieving info for" if action == "i" else "{0}ing".format(action_display.capitalize()), + match.group(2))) + application = None + try: + application = Application.objects.get(id=match.group(2)) + except: + yield from self.discord_message(message.channel, "An Application with that ID doesn't exist.") + return + if action == "i": + # Info + msg = self.build_info(application) + else: + # Action + accept = True if action == "a" else False + if not application.accepted: + application.accepted = accept + application.save() + msg = "App ID **{0}** was successfully {1}.".format(match.group(2), "accepted" if accept else "denied") + api.plugin("accept" if accept else "deny", application.username) + else: + msg = "App ID **{0}** was already {1}.".format(match.group(2), "accepted" if application.accepted else "denied") + yield from self.discord_message(message.channel, msg) + return + # APP INFO WITH PARTIAL NAME SEARCH + match = re.match("[{0}](?:app )?(?:search|info) (\S+)?$".format(self.prefix), message.content) + if match: + search = match.group(1) + yield from self.discord_message(message.channel,"Searching for applications whose username contains '{0}'".format(search)) + applications = Application.objects.filter(username__icontains=search)[:10] + count = Application.objects.filter(username__icontains=search).count() + if count > 0: + if count == 1: + info = self.build_info(applications[0]) + else: + info = "**Found the following applications**" + for app in applications: + info += "\n{0} - {1} ({2})".format(app.id, app.username.replace("_", "\\_"), app.status) + if count > 10: + info += "\n**This is only 10 applications out of {0} found. Please narrow your search if possible.**".format(len(applications)) + else: + info = "No applications matched that search." + yield from self.discord_message(message.channel, info) + # COMPARE DISCORD USERS TO WHITELIST + match = re.match("[{0}]compare".format(self.prefix), message.content) + if match: + yield from self.delete_message(message) + yield from self.send_typing(message.channel) + no_player = [] + no_application = [] + for member in message.server.members: + if member.bot: + continue + name = member.nick if member.nick else member.name + try: + Player.objects.get(username__iexact=name) + except: + no_player.append(name) + try: + Application.objects.get(username__iexact=name) + except: + no_player = no_player[:-1] + no_application.append(name) + header = "**The following users have an application match, but no player match on the whitelist:**\n" + yield from self.discord_message(message.author, "{}```{}```".format(header, "\n".join(no_player))) + header = "**The following users do not have an application or player match on the whitelist:**\n" + yield from self.discord_message(message.author, "{}```{}```".format(header, "\n".join(no_application))) + + + def build_info(self, application): + embed = discord.Embed(colour=discord.Colour(0x417505)) + embed.set_thumbnail( + url="https://minotar.net/helm/{0}/100.png".format(application.username)) + embed.add_field(name="Application ID", value=application.id) + embed.add_field(name="Username", value=application.username.replace("_", "\\_")) + embed.add_field(name="Age", value=application.age) + embed.add_field(name="Type of Player", value=application.player_type) + embed.add_field(name="Ever been banned", value=application.ever_banned) + if application.ever_banned: + embed.add_field(name="Reason for being banned", value=application.ever_banned_explanation) + embed.add_field(name="Reference", value=application.reference) + embed.add_field(name="Read the Rules", value=application.read_rules) + embed.add_field(name="Date", value=application.date_display) + embed.add_field(name="Status", value=application.status) + return embed + + @asyncio.coroutine + def on_error(self, event, *args, **kwargs): + print(sys.exc_info()) + print("Exception raised by " + event) + error = '{0}\n{1}'.format(sys.exc_info()[1], ''.join(traceback.format_tb(sys.exc_info()[2]))) + logger.error(error) + for user in self.error_users: + try: + user = discord.User(id=user) + yield from self.discord_message(user, '```python\n{}```'.format(error)) + except: + pass + + + def run_bot(self): + self.run(self.token) + + +class OreAlert: + # Options + log = os.path.join(settings.MINECRAFT_BASE_DIR, 'logs/latest.log') + purge = 30 # How long until we purge, in minutes + rotate = 5 # How long without input before we assume the log has rotated + notify_start = 5 # How many veins found within the above purge minutes to notify + notify_each = 1 # After the initial alert, how many should be found in addition before more alerts? + notify_ping = 5 # After the initial alert, how many should be found in addition before more pings? + + playerList = [] + + class Player: + + def __init__(self, name, time, prev=[]): + self.name = name + self.time = time + self.prev = prev + + def compare(self, player): + if self.name == player.name: + return True + else: + return False + + def to_string(self): + return str(self.name) + ": " + str(self.time) + ", previous: " + self.prev_to_string() + + def prev_to_string(self): + ret = "" + if len(self.prev) > 0: + for p in self.prev: + ret += str(p) + " " + + return ret.rstrip() + else: + return ret + + def minute_interval(self, start, end): + reverse = False + if start > end: + start, end = end, start + reverse = True + delta = (end.hour - start.hour) * 60 + end.minute - start.minute + (end.second - start.second) / 60.0 + if reverse: + delta = 24 * 60 - delta + return delta + + def follow(self, filename): + thefile = open(filename, 'r', encoding='utf-8') + thefile.seek(0, 2) # Go to the end of the file + start = datetime.datetime.now() + end = datetime.datetime.now() + api.discord_notification('OreAlert has started successfully.') + while True: + line = thefile.readline() + if not line: + if self.minute_interval(start, end) > self.rotate: + thefile.close() + time.sleep(5) + thefile = open(filename, 'r', encoding='utf-8') + thefile.seek(0, 2) # Go to the end of the file + start = datetime.datetime.now() + end = datetime.datetime.now() + # api.discord_notification('OreAlert has closed and re-opened the log...hopefully it just rotated.') + continue + end = end + datetime.timedelta(milliseconds=100) + time.sleep(0.1) # Sleep briefly + continue + start = datetime.datetime.now() + end = datetime.datetime.now() + yield line + + def run_bot(self): + cur_line = "" + try: + loglines = self.follow(self.log) + for line in loglines: + # [00: 03:46] [Server thread / INFO]: [MinecraftManager]: [OreAlert]: Etzelia + if "MinecraftManager" in line and "OreAlert" in line: # Filter out non-OreAlert log statements + cur_time = line.split()[0].replace("[", "").replace("]", "") + if ":" in cur_time: # Make sure we have a time. + time_array = cur_time.split(":") + name = line.split()[-1] + dt = datetime.time(int(time_array[0]), int(time_array[1]), int(time_array[2])) + p = self.Player(name, dt) + new_player = True + for player in self.playerList: + if p.compare(player): + new_player = False + player.prev[:] = [x for x in player.prev if not self.minute_interval(x, + dt) > self.purge] # First, purge any times older than our configured amount + player.prev.append(dt) # Add the new time + if len(player.prev) >= self.notify_start: + # MCM Alert + if len(player.prev) == self.notify_start: + api.create_alert("OreAlert: {0}".format(player.name)) + if len(player.prev) % self.notify_each == 0: + # In-game Notification + api.plugin(api.PLUGIN_STAFF_CHAT, + "OreAlert {0} has found {1} diamond ore veins within {2} minutes.".format( + player.name, len(player.prev), self.purge)) + # Discord Notification + ping = True if len(player.prev) % self.notify_ping == 0 else False + api.discord_notification( + '{0} has found {1} diamond ore veins within {2} minutes.'.format( + player.name.replace("_", "\\_"), len(player.prev), self.purge), ping=ping) + if new_player: + p.prev = [p.time] + self.playerList.append(p) + except KeyboardInterrupt: + api.discord_notification("OreAlert has been stopped manually.") + except: + api.discord_notification("OreAlert has crashed!", ping=True) + + diff --git a/api/urls.py b/api/urls.py new file mode 100644 index 0000000..d2a60ad --- /dev/null +++ b/api/urls.py @@ -0,0 +1,12 @@ +from django.conf.urls import url +from django.views.decorators.csrf import csrf_exempt +import minecraft_manager.api.views as api + +urlpatterns = [ + #API - Password protected + url(r'^web/(?P\w{1,20})/$', csrf_exempt(api.WebAPI.as_view()), name="api-web"), + url(r'^plugin/(?P\w{1,20})/$', csrf_exempt(api.PluginAPI.as_view()), name="api-plugin"), + url(r'^form/(?P\w{1,20})/$', csrf_exempt(api.FormAPI.as_view()), name="api-form"), + url(r'^model/(?P\w{1,20})/$', csrf_exempt(api.ModelAPI.as_view()), name="api-model"), + url(r'^stats/$', csrf_exempt(api.StatsAPI.as_view()), name="api-stats"), +] diff --git a/api/views.py b/api/views.py new file mode 100644 index 0000000..387f32d --- /dev/null +++ b/api/views.py @@ -0,0 +1,423 @@ +from __future__ import absolute_import + +import logging, random, string, discord +from django.contrib.auth.forms import PasswordChangeForm +from django.contrib.auth import update_session_auth_hash +from django.apps import apps +from django.conf import settings +from django.contrib.auth.models import User +from django.http import JsonResponse, HttpResponse +from django.urls import reverse +from django.utils import timezone +from django.views.generic import View +from django.forms import modelform_factory + +import minecraft_manager.forms as MCMForms +from minecraft_manager.models import Player, UserSettings, Application, IP, Ticket, Warning +import minecraft_manager.api.api as mcm_api +import minecraft_manager.utils as mcm_utils +import minecraft_manager.external.stats as mcm_stats + +logger = logging.getLogger(__name__) + + +def request_allowed(request): + is_authenticated = False + if hasattr(request, 'user'): + if hasattr(request.user, 'is_authenticated'): + is_authenticated = request.user.is_authenticated + password = getattr(settings, 'API_PASSWORD', None) + get = request.GET + post= request.POST + request_password = None + if 'api' in get: + request_password = get['api'] + elif 'api' in post: + request_password = post['api'] + correct_password = False + if password and request_password: + correct_password = request_password == password + return is_authenticated or correct_password + + +def clean(model, data): + cleaned = {} + for d in data: + attr = d + if '__' in d: + attr = d.split('__')[0] + if hasattr(model, attr): + cleaned[d] = data[d] + return cleaned + + +def generate_password(): + return "".join([random.choice(string.ascii_letters + string.digits) for idx in range(0, 20)]) + + +class WebAPI(View): + + def get(self, request, keyword): + get = request.GET + data = {'success': False, 'message': 'API failed'} + if request_allowed(request): + keyword = keyword.lower() + if keyword == 'log': + html_global = "" + html_staff = "" + chats = mcm_api.get_chats(request.user.usersettings.default_timezone) + if chats: + for g in chats['global']: + g['text'] = g['text'].replace("<", "<").replace(">", ">") + if request.user.usersettings.show_timestamp_chat: + html_global += "
[{0}] {1}
".format(g['date'], g['text']) + else: + html_global += "
{1}
".format(g['date'], g['text']) + for s in chats['staff']: + s['text'] = s['text'].replace("<", "<").replace(">", ">") + if request.user.usersettings.show_timestamp_chat: + html_staff += "
[{0}] {1}
".format(s['date'], s['text']) + else: + html_staff += "
{1}
".format(s['date'], s['text']) + html = {'global': html_global, 'staff': html_staff} + data = {'chats': chats, 'html': html} + elif keyword == 'online': + query = mcm_api.get_query() + html = "" + for p in query['players']: + html += "
{0}
".format(p) + html += "
" + data = {'query': query, 'html': html} + elif keyword == "coreprotect": + if 'username' in get and 'ip' in get: + user = User.objects.get(username=get['username']) + if user.is_active and user.usersettings.last_ip == get['ip']: + data = {'success': True, 'message': self.access_level(user)} + else: + data = {'success': False, 'message': 'Parameters not set'} + else: + data = {'message': 'Not Authorized', 'success': False} + return JsonResponse(data) + + def post(self, request, keyword): + post = request.POST + data = {} + if request_allowed(request): + keyword = keyword.lower() + if keyword == 'settings' and request.user.usersettings: + for s in [a for a in dir(UserSettings) if not a.startswith('__') and not callable(getattr(UserSettings,a))]: + if s in post: + setattr(request.user.usersettings, s, post[s]) + request.user.usersettings.save() + data = {'success': True, 'message': 'User Settings saved'} + elif keyword == 'password': + form = PasswordChangeForm(request.user, post) + if form.is_valid(): + user = form.save() + update_session_auth_hash(request, user) + return HttpResponse("success") + else: + return HttpResponse(form.as_p()) + elif keyword == 'alert': + form = MCMForms.AlertForm(request.POST) + if form.is_valid(): + if mcm_api.create_alert(form.cleaned_data['message']): + data = {'success': True} + else: + data = {'success': False, 'message': 'Could not create Alerts'} + else: + data = {'success': False, 'message': 'Invalid Message'} + elif keyword == "discord": + if 'message' in post: + ping = post['ping'] if 'ping' in post else False + mcm_api.discord_notification(post['message'], ping=ping) + data = {'success': True, 'message': "Success", 'extra': {'message': post['message'], 'ping': ping}} + else: + data = {'success': False, 'message': 'No message supplied'} + else: + data = {'success': True, 'message': 'Model set to "{0}"'.format(keyword)} + else: + data = {'message': 'Not Authorized', 'success': False} + return JsonResponse(data) + + def access_level(self, user): + access = {'cpp': False, 'cpf': False, 'cpa': False} + if user.has_perm('auth.coreprotect_partial'): + access['cpp'] = True + if user.has_perm('auth.coreprotect_full'): + access['cpf'] = True + if user.has_perm('auth.coreprotect_activity'): + access['cpa'] = True + return access + + +class PluginAPI(View): + + def get(self, request, keyword): + json = {'status': True, 'message': '', 'extra': ''} + if request_allowed(request): + get = request.GET + keyword = keyword.lower() + + return JsonResponse(json) + + def post(self, request, keyword): + json = {'status': True, 'message': '', 'extra': ''} + if request_allowed(request): + post = request.POST + keyword = keyword.lower() + if "application" == keyword: + if Application.objects.filter(username=post['username']).exists(): + application = Application.objects.get(username=post['username']) + if application.accepted is not None: + json['status'] = False + json['message'] = "An application for {0} has already been acted on. Please contact Staff.".format( + post['username']) + return JsonResponse(json) + else: + application.accepted = None + application.age = post['age'] + application.player_type = post['player_type'] + application.ever_banned = False if post['ever_banned'] == "no" else True + application.ever_banned_explanation = post['ever_banned_explanation'] + application.reference = post['reference'] + application.read_rules = post['read_rules'] + else: + application = Application(username=post['username'], age=post['age'], + player_type=post['player_type'], + ever_banned=False if post['ever_banned'] == "no" else True, + ever_banned_explanation= post['ever_banned_explanation'], + reference=post['reference'], read_rules=post['read_rules']) + application.save() + if Player.objects.filter(username__iexact=post['username']).exists(): + player = Player.objects.get(username__iexact=post['username']) + player.application_id = application.id + player.save() + 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) + elif "application_action" == keyword: + if Application.objects.filter(id=post['application_id']).exists(): + application = Application.objects.get(id=post['application_id']) + if application.accepted is not None: + json['status'] = False + json['message'] = "Application was already {0}.".format( + "accepted" if post['action'] == "True" else "denied") + else: + application.accepted = True if post['action'] == "True" else False + 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, + application.id, + "accepted" if post['action'] == "True" else "denied", + post['username'])) + mcm_api.plugin("accept" if post['action'] == "True" else "deny", application.username) + else: + json['status'] = False + json['message'] = "No application found." + return JsonResponse(json) + elif "application_clear" == keyword: + if Application.objects.filter(id=post['application_id']).exists(): + application = Application.objects.get(id=post['application_id']) + if application.accepted is True: + json['status'] = False + json['message'] = "An accepted application can't be cleared." + else: + application.accepted = None + application.save() + json['message'] = "Application was successfully cleared." + else: + json['status'] = False + json['message'] = "No application found." + return JsonResponse(json) + elif "login" == keyword: + player = Player.objects.filter(uuid=post['uuid']).exists() + new_player = False + if not player: + player = Player(uuid=post['uuid'], username=post['username']) + new_player = True + player.first_seen = timezone.now().strftime("%Y-%m-%d") + test_app = Application.objects.filter(username__iexact=post['username']).exists() + if test_app: + test_app = Application.objects.get(username__iexact=post['username']) + player.application = test_app + player.save() + else: + player = Player.objects.get(uuid=post['uuid']) + if player.username != post['username']: + user = User.objects.filter(username__iexact=player.username).exists() + if user: + user = User.objects.get(username__iexact=player.username) + user.username = post['username'].lower() + user.save() + mcm_api.create_alert("Name Change: {0} to {1}.".format(player.username, post['username'])) + player.username = post['username'] + if not player.application: + test_app = Application.objects.filter(username__iexact=post['username']).exists() + if test_app: + test_app = Application.objects.get(username__iexact=player.username) + player.application = test_app + player.save() + test_ip = IP.objects.filter(ip=post['ip'], player=player).exists() + if not test_ip: + ip = IP(ip=post['ip'], player=player) + ip.save() + else: + ip = IP.objects.get(ip=post['ip'], player=player) + player = Player.objects.get(uuid=post['uuid']) + player.last_seen = timezone.now().strftime("%Y-%m-%d") + player.save() + if new_player and ip.associated: + for assoc in ip.associated: + if assoc.uuid is not player.uuid and assoc.is_banned: + mcm_api.plugin("staff", "Server {0}'s IP matches the banned player {1}".format(player.username, assoc.username)) + mcm_api.discord_notification("{0}'s IP matches the banned player {1}".format(player.username, assoc.username), ping=True) + json['status'] = True + json['message'] = "Updated {0}".format(post['username']) + elif "register" == keyword: + player = Player.objects.get(uuid=post['uuid']) + if player.auth_user: + json['status'] = False + json['message'] = "You are already registered. To change your password, contact an Admin." + else: + password = generate_password() + user = User.objects.create_user(username=player.username.lower(), password=password) + user.save() + player.auth_user = user + player.save() + json['message'] = password + elif "ticket" == keyword: + player = Player.objects.get(uuid=post['uuid']) + try: + 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)) + msg = mcm_utils.build_ticket(ticket, link) + json['extra'] = {'id': ticket.id, 'link': link} + mcm_api.discord_mcm(embeds=msg, ping=True) + except: + json['status'] = False + json['message'] = "Error while submitting ticket." + elif "warning" == keyword: + player = Player.objects.get(uuid=post['player']) + staff = Player.objects.get(uuid=post['staff']) + try: + warning = Warning(player=player, message=post['message'], severity=post['severity'], staff=staff.auth_user) + warning.save() + json['message'] = "Warning issued." + link = "{}".format(mcm_utils.url_path(settings.MCM_BASE_LINK, 'dashboard/warning', warning.id)) + msg = mcm_utils.build_warning(warning, link) + mcm_api.discord_mcm(embeds=msg) + except Exception as ex: + json['status'] = False + json['message'] = "Error while issuing warning." + return JsonResponse(json) + + +class FormAPI(View): + + def get(self, request, request_model): + html = "" + if request_allowed(request): + get = request.GET + 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: + form = None + for modelform in MCMForms.__all__(): + if modelform.Meta.model == model: + form = modelform() + break + if not form: + form = modelform_factory(model, exclude=('id',))() + if 'as' in get: + html = getattr(form, 'as_{0}'.format(get['as']))() + else: + html = form.as_p() + return HttpResponse(html) + + def post(self, request, request_model): + html = "" + if request_allowed(request): + 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: + form = None + for modelform in MCMForms.__all__(): + if modelform.Meta.model == model: + form = modelform(post) + break + if not form: + form = modelform_factory(model, exclude=('id',))(post) + if form.is_valid(): + form.save() + html = "Saved" + else: + if 'as' in post: + html = getattr(form, 'as_{0}'.format(post['as']))() + else: + html = form.as_p() + return HttpResponse(html) + + +class ModelAPI(View): + + def get(self, request, request_model): + json = [] + if request_allowed(request): + get = request.GET + 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, get) + objects = model.objects.filter(**keywords).values() + json = [] + for value in objects: + try: + link = "{}".format(mcm_utils.url_path(settings.MCM_BASE_LINK, 'dashboard', request_model, value['id'])) + value['link'] = link + except: + pass + json.append(value) + return JsonResponse(json, safe=False) + + def post(self, request, request_model): + pass + + +class StatsAPI(View): + + def get(self, request): + json = [] + if request_allowed(request): + get = request.GET + if 'stat' in get: + if 'uuid' in get: + json = mcm_stats.one_single(get['uuid'], get['stat']) + elif 'username' in get and Player.objects.filter(username__iexact=get['username']).exists(): + uuid = Player.objects.get(username__iexact=get['username']).uuid + json = mcm_stats.one_single(uuid, get['stat']) + else: + json = mcm_stats.top_ten_stat(get['stat']) + elif 'uuid' in get: + json = mcm_stats.top_ten_player(get['uuid']) + elif 'username' in get: + uuid = Player.objects.get(username__iexact=get['username']).uuid + json = mcm_stats.top_ten_player(uuid) + return JsonResponse(json, safe=False) + + def post(self, request): + pass diff --git a/assets/bots/Discord.bot.py b/assets/bots/Discord.bot.py new file mode 100644 index 0000000..ef882c9 --- /dev/null +++ b/assets/bots/Discord.bot.py @@ -0,0 +1,18 @@ +import os, sys, 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.api.bot import Discord + +token = getattr(settings, 'DISCORD_BOT_TOKEN', None) +bot = Discord(token) +bot.run_bot() diff --git a/assets/bots/OreAlert.bot.py b/assets/bots/OreAlert.bot.py new file mode 100644 index 0000000..16f6617 --- /dev/null +++ b/assets/bots/OreAlert.bot.py @@ -0,0 +1,16 @@ +import os, sys, 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 minecraft_manager.api.bot import OreAlert + +bot = OreAlert() +bot.run_bot() diff --git a/assets/coreprotect/blocks.txt b/assets/coreprotect/blocks.txt new file mode 100644 index 0000000..0bdca39 --- /dev/null +++ b/assets/coreprotect/blocks.txt @@ -0,0 +1,9 @@ +You will need to get this from your CP MySQL tables + +NOTE: CoreProtect does not match up these IDs with the block's in-game ID, so air is not 0, stone is not 1, etc. + +1. mysql -p -e "SELECT id, material FROM co_material_map" > blocks.txt +2. Open blocks.txt +3. Remove the header row +4. Remove spaces/tabs and delimit with a ',' +e.g. 1,minecraft:air \ No newline at end of file diff --git a/assets/migrate.sql b/assets/migrate.sql new file mode 100644 index 0000000..268b167 --- /dev/null +++ b/assets/migrate.sql @@ -0,0 +1,55 @@ +-- 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/external/__init__.py b/external/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/external/stats.py b/external/stats.py new file mode 100644 index 0000000..32922d7 --- /dev/null +++ b/external/stats.py @@ -0,0 +1,85 @@ +import os, json +from minecraft_manager.models import Player +from django.conf import settings + +stats_dir = os.path.join(settings.MINECRAFT_BASE_DIR, getattr(settings, "WORLD", "world"), "stats") + +def get_score(data): + return data['score'] + + +def get_stats(): + stats = {} + for filename in os.listdir(stats_dir): + with open(stats_dir + "/" + filename) as json_file: + j = json.load(json_file)['stats'] + uuid = filename.replace(".json", "") + stats[uuid] = j + return stats + + +def get_names(stats=None, sort=False): + names = [] + if not stats: + stats = get_stats() + for uuid in stats: + for base in stats[uuid]: + for stat in stats[uuid][base]: + name = "{}.{}".format(base, stat) + if name not in names: + names.append(name) + if sort: + names = sorted(names) + return names + + +def top_ten_stat(stat): + stats = get_stats() + top_ten = [] + parts = stat.split('.') + for s in stats: + if parts[0] in stats[s]: + top_ten.append({'uuid': s, 'score': stats[s][parts[0]][parts[1]] if parts[1] in stats[s][parts[0]] else 0}) + top_ten = sorted(top_ten, key=get_score, reverse=True)[:10] + for idx, tt in enumerate(top_ten): + uuid = tt['uuid'] + try: + top_ten[idx]['username'] = Player.objects.get(uuid=uuid).username + except: + top_ten[idx]['username'] = uuid + return top_ten + + +def top_ten_player(uuid): + stats = get_stats() + names = get_names(stats) + top_ten_list = {} + for name in names: + top_ten_list[name] = [] + parts = name.split('.') + for stat in stats: + bases = stats[stat] + if parts[0] in bases: + top_ten_list[name].append({'uuid': stat, 'score': bases[parts[0]][parts[1]] if parts[1] in bases[parts[0]] else 0}) + top_ten_list[name] = sorted(top_ten_list[name], key=get_score, reverse=True) + top_ten = [] + for tt in top_ten_list: + idx = 0 + for ttl in top_ten_list[tt]: + if idx > 10: + break + if ttl['uuid'] == uuid: + ttl['rank'] = idx+1 + ttl['stat'] = tt + top_ten.append(ttl) + break + idx += 1 + return top_ten + + +def one_single(uuid, stat): + stats = get_stats() + if uuid in stats: + return [{'uuid': uuid, 'stat': stat, 'score': stats[uuid][stat]}] + return 0 + diff --git a/external/urls.py b/external/urls.py new file mode 100644 index 0000000..07d2783 --- /dev/null +++ b/external/urls.py @@ -0,0 +1,10 @@ +from django.conf.urls import url +from django.contrib.auth.decorators import login_required +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"), +] diff --git a/external/views.py b/external/views.py new file mode 100644 index 0000000..e3d58ee --- /dev/null +++ b/external/views.py @@ -0,0 +1,158 @@ +from django.views.generic import View +from django.shortcuts import render, reverse, redirect +from django.conf import settings +from django.utils.decorators import method_decorator +from django.views.decorators.csrf import csrf_exempt +from minecraft_manager.forms import ApplicationForm, TicketForm +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 + + +def config(): + data = {} + data['discord_invite'] = getattr(settings, "DISCORD_INVITE", "#") + + dynmap_url = getattr(settings, "DYNMAP_URL", "") + data['dynmap_url'] = dynmap_url + + dynmap_static_url = getattr(settings, "DYNMAP_STATIC_URL", dynmap_url) + data['dynmap_static_url'] = dynmap_static_url + + dynmap_world = getattr(settings, "WORLD", "world") + data['dynmap_world'] = dynmap_world + + world_border = getattr(settings, "WORLD_BORDER", 100) + data['world_border'] = world_border + + data['target'] = "_blank" if dynmap_url else "_self" + + x = random.randint(-world_border, world_border) + data['x'] = x + + z = random.randint(-world_border, world_border) + data['z'] = z + + uri = "?worldname={}&mapname=surface&zoom=5&x={}&y=64&z={}".format(dynmap_world, x, z) + data['map_url'] = "{}{}".format(dynmap_url, uri) + data['static_url'] = "{}{}".format(dynmap_static_url, uri) + + return data + + +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'])) + + return data + + +@method_decorator(csrf_exempt, name='dispatch') +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")}) + + 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: + app = form.save() + msg = mcm_utils.build_application(app) + mcm_api.discord_mcm(message='New Application!', embeds=msg) + mcm_api.plugin("application", "{0} {1}".format(form.data['username'], app.id)) + else: + for error in captcha.errors: + form.add_error(None, error) + if not valid_username: + form.add_error(None, "That username is not a premium Minecraft account") + 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")}) + + +@method_decorator(csrf_exempt, name='dispatch') +class Ticket(View): + + def get(self, request): + form = TicketForm() + return render(request, 'minecraft_manager/external/ticket.html', + {'form': form.as_p(), 'valid': False, 'map': config(), + 'captcha': hasattr(settings, "CAPTCHA_SECRET")}) + + def post(self, request): + post = request.POST.copy() + username = post['player'] + if 'player' in post and Player.objects.filter(username__iexact=post['player']).exists(): + player = Player.objects.get(username__iexact=post['player']) + post.update({'player': player.id}) + form = TicketForm(post) + captcha = mcm_utils.Captcha(request.POST) + valid = form.is_valid() + if 'player' not in post: + # Why did I make Player nullable??? + valid = False + form.add_error(None, "You must enter your IGN") + 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)) + msg = mcm_utils.build_ticket(ticket, link) + mcm_api.discord_mcm(message="New Ticket", embeds=msg, ping=True) + mcm_api.plugin("ticket", "{0} {1} {2}".format(username, ticket.id, link)) + else: + for error in captcha.errors: + form.add_error(None, error) + if 'player' in form.errors: + form.errors['player'][0] = 'You must enter your IGN.' + form.data = form.data.copy() + 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")}) + + +@method_decorator(csrf_exempt, name='dispatch') +class Stats(View): + + def get(self, request): + get = request.GET + results = None + if 'stat' in get and get['stat']: + results = mcm_stats.top_ten_stat(get['stat']) + return render(request, 'minecraft_manager/external/stats_all.html', {'map': config(), 'stats': mcm_stats.get_names(sort=True), 'results': results}) + + def post(self, request): + pass + + +@method_decorator(csrf_exempt, name='dispatch') +class StatsPlayer(View): + + def get(self, request): + get = request.GET + results = None + if 'username' in get and get['username']: + if Player.objects.filter(username__iexact=get['username']).exists(): + uuid = Player.objects.get(username__iexact=get['username']).uuid + results = mcm_stats.top_ten_player(uuid) + if 'uuid' in get and get['uuid']: + results = mcm_stats.top_ten_player(get['uuid']) + return render(request, 'minecraft_manager/external/stats_single.html', {'map': config(), 'results': results}) + + def post(self, request): + pass + + + diff --git a/forms.py b/forms.py new file mode 100644 index 0000000..b9003e4 --- /dev/null +++ b/forms.py @@ -0,0 +1,53 @@ +from django.forms import ModelForm, Textarea, HiddenInput, TextInput +from minecraft_manager.models import UserSettings, Application, Alert, Ticket, Warning, Note + + +def __all__(): + return [UserSettingsForm, ApplicationForm, AlertForm, TicketForm, WarningForm, NoteForm] + + +class UserSettingsForm(ModelForm): + class Meta: + model = UserSettings + fields = ['default_results', 'default_theme', 'default_timezone', 'search_player_ip', 'show_timestamp_chat'] + + +class ApplicationForm(ModelForm): + class Meta: + model = Application + fields = ['username', 'age', 'player_type', 'ever_banned', 'ever_banned_explanation', 'reference', 'read_rules'] + + +class AlertForm(ModelForm): + class Meta: + model = Alert + fields = ['message'] + + +class TicketForm(ModelForm): + class Meta: + model = Ticket + fields = ['player', 'message', 'priority', 'world', 'x', 'y', 'z'] + widgets = { + 'player': TextInput, + 'message': Textarea, + } + + +class WarningForm(ModelForm): + class Meta: + model = Warning + fields = ['player', 'message', 'severity'] + widgets = { + 'message': Textarea + } + + +class NoteForm(ModelForm): + class Meta: + model = Note + fields = ['ref_id', 'ref_table', 'message'] + widgets = { + 'ref_id': HiddenInput, + 'ref_table': HiddenInput + } diff --git a/middleware.py b/middleware.py new file mode 100644 index 0000000..69fe329 --- /dev/null +++ b/middleware.py @@ -0,0 +1,16 @@ +import pytz + +from django.utils import timezone +from django.utils.deprecation import MiddlewareMixin + + +class TimezoneMiddleware(MiddlewareMixin): + def process_request(self, request): + tzname = request.session.get('django_timezone') + if tzname: + timezone.activate(pytz.timezone(tzname)) + else: + try: + timezone.activate(pytz.timezone(request.user.usersettings.default_timezone)) + except: + timezone.deactivate() \ No newline at end of file diff --git a/migrations/0001_initial.py b/migrations/0001_initial.py new file mode 100644 index 0000000..afc690b --- /dev/null +++ b/migrations/0001_initial.py @@ -0,0 +1,128 @@ +# Generated by Django 2.0.5 on 2018-05-30 02:43 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Alert', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('message', models.TextField(max_length=1000)), + ('seen', models.BooleanField(default=False)), + ('date', models.DateTimeField(auto_now_add=True, null=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='Application', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('username', models.CharField(max_length=20, unique=True, verbose_name='Minecraft Username')), + ('age', models.PositiveSmallIntegerField()), + ('player_type', models.TextField(max_length=300, verbose_name='What type of player are you?')), + ('ever_banned', models.BooleanField(default=False, verbose_name='Have you ever been banned?')), + ('ever_banned_explanation', models.TextField(blank=True, max_length=300, null=True, verbose_name='If you were previously banned, will you share why?')), + ('reference', models.CharField(blank=True, max_length=50, null=True, verbose_name='Were you referred to our server?')), + ('read_rules', models.CharField(max_length=10, verbose_name='Have you read the rules?')), + ('accepted', models.NullBooleanField()), + ('date', models.DateTimeField(auto_now_add=True, null=True)), + ], + ), + migrations.CreateModel( + name='IP', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('ip', models.CharField(max_length=30)), + ], + options={ + 'verbose_name': 'IP', + 'verbose_name_plural': 'IPs', + }, + ), + migrations.CreateModel( + name='Note', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('ref_table', models.CharField(choices=[('PL', 'Player'), ('TI', 'Ticket'), ('WA', 'Warning')], max_length=2)), + ('ref_id', models.CharField(max_length=4)), + ('message', models.TextField(max_length=1000)), + ('last_update', models.DateTimeField(auto_now_add=True, null=True)), + ('date', models.DateTimeField(auto_now_add=True, null=True)), + ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='Player', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('uuid', models.CharField(max_length=36, unique=True)), + ('username', models.CharField(max_length=20)), + ('rank', models.CharField(choices=[('GU', 'Guest'), ('ME', 'Member'), ('DO', 'Donor'), ('JM', 'J-Mod'), ('MO', 'Mod'), ('SM', 'S-Mod'), ('AD', 'Admin')], default='GU', max_length=10)), + ('first_seen', models.DateField(blank=True, null=True)), + ('last_seen', models.DateField(blank=True, null=True)), + ('application', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='minecraft_manager.Application')), + ('auth_user', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='Ticket', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('message', models.CharField(max_length=500)), + ('priority', models.CharField(blank=True, choices=[('L', 'Low'), ('M', 'Medium'), ('H', 'High')], max_length=1)), + ('type', models.CharField(blank=True, choices=[('I', 'In-Game'), ('S', 'Server'), ('D', 'Discord'), ('W', 'Web')], max_length=1)), + ('resolved', models.BooleanField(default=False)), + ('world', models.CharField(blank=True, choices=[('O', 'Overworld'), ('N', 'Nether'), ('E', 'The End')], max_length=1, null=True)), + ('x', models.CharField(blank=True, max_length=20, null=True)), + ('y', models.CharField(blank=True, max_length=20, null=True)), + ('z', models.CharField(blank=True, max_length=20, null=True)), + ('date', models.DateTimeField(auto_now_add=True, null=True)), + ('player', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='ticket_player', to='minecraft_manager.Player')), + ('staff', models.ForeignKey(blank=True, limit_choices_to=models.Q(('rank', 'JM'), ('rank', 'MO'), ('rank', 'SM'), ('rank', 'AD'), _connector='OR'), null=True, on_delete=django.db.models.deletion.CASCADE, related_name='ticket_staff', to='minecraft_manager.Player')), + ], + ), + migrations.CreateModel( + name='UserSettings', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('default_results', models.SmallIntegerField(choices=[(10, '10'), (25, '25'), (50, '50'), (100, '100'), (-1, 'All')], default=10, verbose_name='Default Results')), + ('default_theme', models.CharField(choices=[('DE', 'Default'), ('DA', 'Dark'), ('SO', 'Solar'), ('SL', 'Slate')], default='DE', max_length=2, verbose_name='Theme')), + ('default_timezone', models.CharField(choices=[('Africa/Abidjan', 'Africa/Abidjan'), ('Africa/Accra', 'Africa/Accra'), ('Africa/Addis_Ababa', 'Africa/Addis_Ababa'), ('Africa/Algiers', 'Africa/Algiers'), ('Africa/Asmara', 'Africa/Asmara'), ('Africa/Bamako', 'Africa/Bamako'), ('Africa/Bangui', 'Africa/Bangui'), ('Africa/Banjul', 'Africa/Banjul'), ('Africa/Bissau', 'Africa/Bissau'), ('Africa/Blantyre', 'Africa/Blantyre'), ('Africa/Brazzaville', 'Africa/Brazzaville'), ('Africa/Bujumbura', 'Africa/Bujumbura'), ('Africa/Cairo', 'Africa/Cairo'), ('Africa/Casablanca', 'Africa/Casablanca'), ('Africa/Ceuta', 'Africa/Ceuta'), ('Africa/Conakry', 'Africa/Conakry'), ('Africa/Dakar', 'Africa/Dakar'), ('Africa/Dar_es_Salaam', 'Africa/Dar_es_Salaam'), ('Africa/Djibouti', 'Africa/Djibouti'), ('Africa/Douala', 'Africa/Douala'), ('Africa/El_Aaiun', 'Africa/El_Aaiun'), ('Africa/Freetown', 'Africa/Freetown'), ('Africa/Gaborone', 'Africa/Gaborone'), ('Africa/Harare', 'Africa/Harare'), ('Africa/Johannesburg', 'Africa/Johannesburg'), ('Africa/Juba', 'Africa/Juba'), ('Africa/Kampala', 'Africa/Kampala'), ('Africa/Khartoum', 'Africa/Khartoum'), ('Africa/Kigali', 'Africa/Kigali'), ('Africa/Kinshasa', 'Africa/Kinshasa'), ('Africa/Lagos', 'Africa/Lagos'), ('Africa/Libreville', 'Africa/Libreville'), ('Africa/Lome', 'Africa/Lome'), ('Africa/Luanda', 'Africa/Luanda'), ('Africa/Lubumbashi', 'Africa/Lubumbashi'), ('Africa/Lusaka', 'Africa/Lusaka'), ('Africa/Malabo', 'Africa/Malabo'), ('Africa/Maputo', 'Africa/Maputo'), ('Africa/Maseru', 'Africa/Maseru'), ('Africa/Mbabane', 'Africa/Mbabane'), ('Africa/Mogadishu', 'Africa/Mogadishu'), ('Africa/Monrovia', 'Africa/Monrovia'), ('Africa/Nairobi', 'Africa/Nairobi'), ('Africa/Ndjamena', 'Africa/Ndjamena'), ('Africa/Niamey', 'Africa/Niamey'), ('Africa/Nouakchott', 'Africa/Nouakchott'), ('Africa/Ouagadougou', 'Africa/Ouagadougou'), ('Africa/Porto-Novo', 'Africa/Porto-Novo'), ('Africa/Sao_Tome', 'Africa/Sao_Tome'), ('Africa/Tripoli', 'Africa/Tripoli'), ('Africa/Tunis', 'Africa/Tunis'), ('Africa/Windhoek', 'Africa/Windhoek'), ('America/Adak', 'America/Adak'), ('America/Anchorage', 'America/Anchorage'), ('America/Anguilla', 'America/Anguilla'), ('America/Antigua', 'America/Antigua'), ('America/Araguaina', 'America/Araguaina'), ('America/Argentina/Buenos_Aires', 'America/Argentina/Buenos_Aires'), ('America/Argentina/Catamarca', 'America/Argentina/Catamarca'), ('America/Argentina/Cordoba', 'America/Argentina/Cordoba'), ('America/Argentina/Jujuy', 'America/Argentina/Jujuy'), ('America/Argentina/La_Rioja', 'America/Argentina/La_Rioja'), ('America/Argentina/Mendoza', 'America/Argentina/Mendoza'), ('America/Argentina/Rio_Gallegos', 'America/Argentina/Rio_Gallegos'), ('America/Argentina/Salta', 'America/Argentina/Salta'), ('America/Argentina/San_Juan', 'America/Argentina/San_Juan'), ('America/Argentina/San_Luis', 'America/Argentina/San_Luis'), ('America/Argentina/Tucuman', 'America/Argentina/Tucuman'), ('America/Argentina/Ushuaia', 'America/Argentina/Ushuaia'), ('America/Aruba', 'America/Aruba'), ('America/Asuncion', 'America/Asuncion'), ('America/Atikokan', 'America/Atikokan'), ('America/Bahia', 'America/Bahia'), ('America/Bahia_Banderas', 'America/Bahia_Banderas'), ('America/Barbados', 'America/Barbados'), ('America/Belem', 'America/Belem'), ('America/Belize', 'America/Belize'), ('America/Blanc-Sablon', 'America/Blanc-Sablon'), ('America/Boa_Vista', 'America/Boa_Vista'), ('America/Bogota', 'America/Bogota'), ('America/Boise', 'America/Boise'), ('America/Cambridge_Bay', 'America/Cambridge_Bay'), ('America/Campo_Grande', 'America/Campo_Grande'), ('America/Cancun', 'America/Cancun'), ('America/Caracas', 'America/Caracas'), ('America/Cayenne', 'America/Cayenne'), ('America/Cayman', 'America/Cayman'), ('America/Chicago', 'America/Chicago'), ('America/Chihuahua', 'America/Chihuahua'), ('America/Costa_Rica', 'America/Costa_Rica'), ('America/Creston', 'America/Creston'), ('America/Cuiaba', 'America/Cuiaba'), ('America/Curacao', 'America/Curacao'), ('America/Danmarkshavn', 'America/Danmarkshavn'), ('America/Dawson', 'America/Dawson'), ('America/Dawson_Creek', 'America/Dawson_Creek'), ('America/Denver', 'America/Denver'), ('America/Detroit', 'America/Detroit'), ('America/Dominica', 'America/Dominica'), ('America/Edmonton', 'America/Edmonton'), ('America/Eirunepe', 'America/Eirunepe'), ('America/El_Salvador', 'America/El_Salvador'), ('America/Fort_Nelson', 'America/Fort_Nelson'), ('America/Fortaleza', 'America/Fortaleza'), ('America/Glace_Bay', 'America/Glace_Bay'), ('America/Godthab', 'America/Godthab'), ('America/Goose_Bay', 'America/Goose_Bay'), ('America/Grand_Turk', 'America/Grand_Turk'), ('America/Grenada', 'America/Grenada'), ('America/Guadeloupe', 'America/Guadeloupe'), ('America/Guatemala', 'America/Guatemala'), ('America/Guayaquil', 'America/Guayaquil'), ('America/Guyana', 'America/Guyana'), ('America/Halifax', 'America/Halifax'), ('America/Havana', 'America/Havana'), ('America/Hermosillo', 'America/Hermosillo'), ('America/Indiana/Indianapolis', 'America/Indiana/Indianapolis'), ('America/Indiana/Knox', 'America/Indiana/Knox'), ('America/Indiana/Marengo', 'America/Indiana/Marengo'), ('America/Indiana/Petersburg', 'America/Indiana/Petersburg'), ('America/Indiana/Tell_City', 'America/Indiana/Tell_City'), ('America/Indiana/Vevay', 'America/Indiana/Vevay'), ('America/Indiana/Vincennes', 'America/Indiana/Vincennes'), ('America/Indiana/Winamac', 'America/Indiana/Winamac'), ('America/Inuvik', 'America/Inuvik'), ('America/Iqaluit', 'America/Iqaluit'), ('America/Jamaica', 'America/Jamaica'), ('America/Juneau', 'America/Juneau'), ('America/Kentucky/Louisville', 'America/Kentucky/Louisville'), ('America/Kentucky/Monticello', 'America/Kentucky/Monticello'), ('America/Kralendijk', 'America/Kralendijk'), ('America/La_Paz', 'America/La_Paz'), ('America/Lima', 'America/Lima'), ('America/Los_Angeles', 'America/Los_Angeles'), ('America/Lower_Princes', 'America/Lower_Princes'), ('America/Maceio', 'America/Maceio'), ('America/Managua', 'America/Managua'), ('America/Manaus', 'America/Manaus'), ('America/Marigot', 'America/Marigot'), ('America/Martinique', 'America/Martinique'), ('America/Matamoros', 'America/Matamoros'), ('America/Mazatlan', 'America/Mazatlan'), ('America/Menominee', 'America/Menominee'), ('America/Merida', 'America/Merida'), ('America/Metlakatla', 'America/Metlakatla'), ('America/Mexico_City', 'America/Mexico_City'), ('America/Miquelon', 'America/Miquelon'), ('America/Moncton', 'America/Moncton'), ('America/Monterrey', 'America/Monterrey'), ('America/Montevideo', 'America/Montevideo'), ('America/Montserrat', 'America/Montserrat'), ('America/Nassau', 'America/Nassau'), ('America/New_York', 'America/New_York'), ('America/Nipigon', 'America/Nipigon'), ('America/Nome', 'America/Nome'), ('America/Noronha', 'America/Noronha'), ('America/North_Dakota/Beulah', 'America/North_Dakota/Beulah'), ('America/North_Dakota/Center', 'America/North_Dakota/Center'), ('America/North_Dakota/New_Salem', 'America/North_Dakota/New_Salem'), ('America/Ojinaga', 'America/Ojinaga'), ('America/Panama', 'America/Panama'), ('America/Pangnirtung', 'America/Pangnirtung'), ('America/Paramaribo', 'America/Paramaribo'), ('America/Phoenix', 'America/Phoenix'), ('America/Port-au-Prince', 'America/Port-au-Prince'), ('America/Port_of_Spain', 'America/Port_of_Spain'), ('America/Porto_Velho', 'America/Porto_Velho'), ('America/Puerto_Rico', 'America/Puerto_Rico'), ('America/Punta_Arenas', 'America/Punta_Arenas'), ('America/Rainy_River', 'America/Rainy_River'), ('America/Rankin_Inlet', 'America/Rankin_Inlet'), ('America/Recife', 'America/Recife'), ('America/Regina', 'America/Regina'), ('America/Resolute', 'America/Resolute'), ('America/Rio_Branco', 'America/Rio_Branco'), ('America/Santarem', 'America/Santarem'), ('America/Santiago', 'America/Santiago'), ('America/Santo_Domingo', 'America/Santo_Domingo'), ('America/Sao_Paulo', 'America/Sao_Paulo'), ('America/Scoresbysund', 'America/Scoresbysund'), ('America/Sitka', 'America/Sitka'), ('America/St_Barthelemy', 'America/St_Barthelemy'), ('America/St_Johns', 'America/St_Johns'), ('America/St_Kitts', 'America/St_Kitts'), ('America/St_Lucia', 'America/St_Lucia'), ('America/St_Thomas', 'America/St_Thomas'), ('America/St_Vincent', 'America/St_Vincent'), ('America/Swift_Current', 'America/Swift_Current'), ('America/Tegucigalpa', 'America/Tegucigalpa'), ('America/Thule', 'America/Thule'), ('America/Thunder_Bay', 'America/Thunder_Bay'), ('America/Tijuana', 'America/Tijuana'), ('America/Toronto', 'America/Toronto'), ('America/Tortola', 'America/Tortola'), ('America/Vancouver', 'America/Vancouver'), ('America/Whitehorse', 'America/Whitehorse'), ('America/Winnipeg', 'America/Winnipeg'), ('America/Yakutat', 'America/Yakutat'), ('America/Yellowknife', 'America/Yellowknife'), ('Antarctica/Casey', 'Antarctica/Casey'), ('Antarctica/Davis', 'Antarctica/Davis'), ('Antarctica/DumontDUrville', 'Antarctica/DumontDUrville'), ('Antarctica/Macquarie', 'Antarctica/Macquarie'), ('Antarctica/Mawson', 'Antarctica/Mawson'), ('Antarctica/McMurdo', 'Antarctica/McMurdo'), ('Antarctica/Palmer', 'Antarctica/Palmer'), ('Antarctica/Rothera', 'Antarctica/Rothera'), ('Antarctica/Syowa', 'Antarctica/Syowa'), ('Antarctica/Troll', 'Antarctica/Troll'), ('Antarctica/Vostok', 'Antarctica/Vostok'), ('Arctic/Longyearbyen', 'Arctic/Longyearbyen'), ('Asia/Aden', 'Asia/Aden'), ('Asia/Almaty', 'Asia/Almaty'), ('Asia/Amman', 'Asia/Amman'), ('Asia/Anadyr', 'Asia/Anadyr'), ('Asia/Aqtau', 'Asia/Aqtau'), ('Asia/Aqtobe', 'Asia/Aqtobe'), ('Asia/Ashgabat', 'Asia/Ashgabat'), ('Asia/Atyrau', 'Asia/Atyrau'), ('Asia/Baghdad', 'Asia/Baghdad'), ('Asia/Bahrain', 'Asia/Bahrain'), ('Asia/Baku', 'Asia/Baku'), ('Asia/Bangkok', 'Asia/Bangkok'), ('Asia/Barnaul', 'Asia/Barnaul'), ('Asia/Beirut', 'Asia/Beirut'), ('Asia/Bishkek', 'Asia/Bishkek'), ('Asia/Brunei', 'Asia/Brunei'), ('Asia/Chita', 'Asia/Chita'), ('Asia/Choibalsan', 'Asia/Choibalsan'), ('Asia/Colombo', 'Asia/Colombo'), ('Asia/Damascus', 'Asia/Damascus'), ('Asia/Dhaka', 'Asia/Dhaka'), ('Asia/Dili', 'Asia/Dili'), ('Asia/Dubai', 'Asia/Dubai'), ('Asia/Dushanbe', 'Asia/Dushanbe'), ('Asia/Famagusta', 'Asia/Famagusta'), ('Asia/Gaza', 'Asia/Gaza'), ('Asia/Hebron', 'Asia/Hebron'), ('Asia/Ho_Chi_Minh', 'Asia/Ho_Chi_Minh'), ('Asia/Hong_Kong', 'Asia/Hong_Kong'), ('Asia/Hovd', 'Asia/Hovd'), ('Asia/Irkutsk', 'Asia/Irkutsk'), ('Asia/Jakarta', 'Asia/Jakarta'), ('Asia/Jayapura', 'Asia/Jayapura'), ('Asia/Jerusalem', 'Asia/Jerusalem'), ('Asia/Kabul', 'Asia/Kabul'), ('Asia/Kamchatka', 'Asia/Kamchatka'), ('Asia/Karachi', 'Asia/Karachi'), ('Asia/Kathmandu', 'Asia/Kathmandu'), ('Asia/Khandyga', 'Asia/Khandyga'), ('Asia/Kolkata', 'Asia/Kolkata'), ('Asia/Krasnoyarsk', 'Asia/Krasnoyarsk'), ('Asia/Kuala_Lumpur', 'Asia/Kuala_Lumpur'), ('Asia/Kuching', 'Asia/Kuching'), ('Asia/Kuwait', 'Asia/Kuwait'), ('Asia/Macau', 'Asia/Macau'), ('Asia/Magadan', 'Asia/Magadan'), ('Asia/Makassar', 'Asia/Makassar'), ('Asia/Manila', 'Asia/Manila'), ('Asia/Muscat', 'Asia/Muscat'), ('Asia/Nicosia', 'Asia/Nicosia'), ('Asia/Novokuznetsk', 'Asia/Novokuznetsk'), ('Asia/Novosibirsk', 'Asia/Novosibirsk'), ('Asia/Omsk', 'Asia/Omsk'), ('Asia/Oral', 'Asia/Oral'), ('Asia/Phnom_Penh', 'Asia/Phnom_Penh'), ('Asia/Pontianak', 'Asia/Pontianak'), ('Asia/Pyongyang', 'Asia/Pyongyang'), ('Asia/Qatar', 'Asia/Qatar'), ('Asia/Qyzylorda', 'Asia/Qyzylorda'), ('Asia/Riyadh', 'Asia/Riyadh'), ('Asia/Sakhalin', 'Asia/Sakhalin'), ('Asia/Samarkand', 'Asia/Samarkand'), ('Asia/Seoul', 'Asia/Seoul'), ('Asia/Shanghai', 'Asia/Shanghai'), ('Asia/Singapore', 'Asia/Singapore'), ('Asia/Srednekolymsk', 'Asia/Srednekolymsk'), ('Asia/Taipei', 'Asia/Taipei'), ('Asia/Tashkent', 'Asia/Tashkent'), ('Asia/Tbilisi', 'Asia/Tbilisi'), ('Asia/Tehran', 'Asia/Tehran'), ('Asia/Thimphu', 'Asia/Thimphu'), ('Asia/Tokyo', 'Asia/Tokyo'), ('Asia/Tomsk', 'Asia/Tomsk'), ('Asia/Ulaanbaatar', 'Asia/Ulaanbaatar'), ('Asia/Urumqi', 'Asia/Urumqi'), ('Asia/Ust-Nera', 'Asia/Ust-Nera'), ('Asia/Vientiane', 'Asia/Vientiane'), ('Asia/Vladivostok', 'Asia/Vladivostok'), ('Asia/Yakutsk', 'Asia/Yakutsk'), ('Asia/Yangon', 'Asia/Yangon'), ('Asia/Yekaterinburg', 'Asia/Yekaterinburg'), ('Asia/Yerevan', 'Asia/Yerevan'), ('Atlantic/Azores', 'Atlantic/Azores'), ('Atlantic/Bermuda', 'Atlantic/Bermuda'), ('Atlantic/Canary', 'Atlantic/Canary'), ('Atlantic/Cape_Verde', 'Atlantic/Cape_Verde'), ('Atlantic/Faroe', 'Atlantic/Faroe'), ('Atlantic/Madeira', 'Atlantic/Madeira'), ('Atlantic/Reykjavik', 'Atlantic/Reykjavik'), ('Atlantic/South_Georgia', 'Atlantic/South_Georgia'), ('Atlantic/St_Helena', 'Atlantic/St_Helena'), ('Atlantic/Stanley', 'Atlantic/Stanley'), ('Australia/Adelaide', 'Australia/Adelaide'), ('Australia/Brisbane', 'Australia/Brisbane'), ('Australia/Broken_Hill', 'Australia/Broken_Hill'), ('Australia/Currie', 'Australia/Currie'), ('Australia/Darwin', 'Australia/Darwin'), ('Australia/Eucla', 'Australia/Eucla'), ('Australia/Hobart', 'Australia/Hobart'), ('Australia/Lindeman', 'Australia/Lindeman'), ('Australia/Lord_Howe', 'Australia/Lord_Howe'), ('Australia/Melbourne', 'Australia/Melbourne'), ('Australia/Perth', 'Australia/Perth'), ('Australia/Sydney', 'Australia/Sydney'), ('Canada/Atlantic', 'Canada/Atlantic'), ('Canada/Central', 'Canada/Central'), ('Canada/Eastern', 'Canada/Eastern'), ('Canada/Mountain', 'Canada/Mountain'), ('Canada/Newfoundland', 'Canada/Newfoundland'), ('Canada/Pacific', 'Canada/Pacific'), ('Europe/Amsterdam', 'Europe/Amsterdam'), ('Europe/Andorra', 'Europe/Andorra'), ('Europe/Astrakhan', 'Europe/Astrakhan'), ('Europe/Athens', 'Europe/Athens'), ('Europe/Belgrade', 'Europe/Belgrade'), ('Europe/Berlin', 'Europe/Berlin'), ('Europe/Bratislava', 'Europe/Bratislava'), ('Europe/Brussels', 'Europe/Brussels'), ('Europe/Bucharest', 'Europe/Bucharest'), ('Europe/Budapest', 'Europe/Budapest'), ('Europe/Busingen', 'Europe/Busingen'), ('Europe/Chisinau', 'Europe/Chisinau'), ('Europe/Copenhagen', 'Europe/Copenhagen'), ('Europe/Dublin', 'Europe/Dublin'), ('Europe/Gibraltar', 'Europe/Gibraltar'), ('Europe/Guernsey', 'Europe/Guernsey'), ('Europe/Helsinki', 'Europe/Helsinki'), ('Europe/Isle_of_Man', 'Europe/Isle_of_Man'), ('Europe/Istanbul', 'Europe/Istanbul'), ('Europe/Jersey', 'Europe/Jersey'), ('Europe/Kaliningrad', 'Europe/Kaliningrad'), ('Europe/Kiev', 'Europe/Kiev'), ('Europe/Kirov', 'Europe/Kirov'), ('Europe/Lisbon', 'Europe/Lisbon'), ('Europe/Ljubljana', 'Europe/Ljubljana'), ('Europe/London', 'Europe/London'), ('Europe/Luxembourg', 'Europe/Luxembourg'), ('Europe/Madrid', 'Europe/Madrid'), ('Europe/Malta', 'Europe/Malta'), ('Europe/Mariehamn', 'Europe/Mariehamn'), ('Europe/Minsk', 'Europe/Minsk'), ('Europe/Monaco', 'Europe/Monaco'), ('Europe/Moscow', 'Europe/Moscow'), ('Europe/Oslo', 'Europe/Oslo'), ('Europe/Paris', 'Europe/Paris'), ('Europe/Podgorica', 'Europe/Podgorica'), ('Europe/Prague', 'Europe/Prague'), ('Europe/Riga', 'Europe/Riga'), ('Europe/Rome', 'Europe/Rome'), ('Europe/Samara', 'Europe/Samara'), ('Europe/San_Marino', 'Europe/San_Marino'), ('Europe/Sarajevo', 'Europe/Sarajevo'), ('Europe/Saratov', 'Europe/Saratov'), ('Europe/Simferopol', 'Europe/Simferopol'), ('Europe/Skopje', 'Europe/Skopje'), ('Europe/Sofia', 'Europe/Sofia'), ('Europe/Stockholm', 'Europe/Stockholm'), ('Europe/Tallinn', 'Europe/Tallinn'), ('Europe/Tirane', 'Europe/Tirane'), ('Europe/Ulyanovsk', 'Europe/Ulyanovsk'), ('Europe/Uzhgorod', 'Europe/Uzhgorod'), ('Europe/Vaduz', 'Europe/Vaduz'), ('Europe/Vatican', 'Europe/Vatican'), ('Europe/Vienna', 'Europe/Vienna'), ('Europe/Vilnius', 'Europe/Vilnius'), ('Europe/Volgograd', 'Europe/Volgograd'), ('Europe/Warsaw', 'Europe/Warsaw'), ('Europe/Zagreb', 'Europe/Zagreb'), ('Europe/Zaporozhye', 'Europe/Zaporozhye'), ('Europe/Zurich', 'Europe/Zurich'), ('GMT', 'GMT'), ('Indian/Antananarivo', 'Indian/Antananarivo'), ('Indian/Chagos', 'Indian/Chagos'), ('Indian/Christmas', 'Indian/Christmas'), ('Indian/Cocos', 'Indian/Cocos'), ('Indian/Comoro', 'Indian/Comoro'), ('Indian/Kerguelen', 'Indian/Kerguelen'), ('Indian/Mahe', 'Indian/Mahe'), ('Indian/Maldives', 'Indian/Maldives'), ('Indian/Mauritius', 'Indian/Mauritius'), ('Indian/Mayotte', 'Indian/Mayotte'), ('Indian/Reunion', 'Indian/Reunion'), ('Pacific/Apia', 'Pacific/Apia'), ('Pacific/Auckland', 'Pacific/Auckland'), ('Pacific/Bougainville', 'Pacific/Bougainville'), ('Pacific/Chatham', 'Pacific/Chatham'), ('Pacific/Chuuk', 'Pacific/Chuuk'), ('Pacific/Easter', 'Pacific/Easter'), ('Pacific/Efate', 'Pacific/Efate'), ('Pacific/Enderbury', 'Pacific/Enderbury'), ('Pacific/Fakaofo', 'Pacific/Fakaofo'), ('Pacific/Fiji', 'Pacific/Fiji'), ('Pacific/Funafuti', 'Pacific/Funafuti'), ('Pacific/Galapagos', 'Pacific/Galapagos'), ('Pacific/Gambier', 'Pacific/Gambier'), ('Pacific/Guadalcanal', 'Pacific/Guadalcanal'), ('Pacific/Guam', 'Pacific/Guam'), ('Pacific/Honolulu', 'Pacific/Honolulu'), ('Pacific/Kiritimati', 'Pacific/Kiritimati'), ('Pacific/Kosrae', 'Pacific/Kosrae'), ('Pacific/Kwajalein', 'Pacific/Kwajalein'), ('Pacific/Majuro', 'Pacific/Majuro'), ('Pacific/Marquesas', 'Pacific/Marquesas'), ('Pacific/Midway', 'Pacific/Midway'), ('Pacific/Nauru', 'Pacific/Nauru'), ('Pacific/Niue', 'Pacific/Niue'), ('Pacific/Norfolk', 'Pacific/Norfolk'), ('Pacific/Noumea', 'Pacific/Noumea'), ('Pacific/Pago_Pago', 'Pacific/Pago_Pago'), ('Pacific/Palau', 'Pacific/Palau'), ('Pacific/Pitcairn', 'Pacific/Pitcairn'), ('Pacific/Pohnpei', 'Pacific/Pohnpei'), ('Pacific/Port_Moresby', 'Pacific/Port_Moresby'), ('Pacific/Rarotonga', 'Pacific/Rarotonga'), ('Pacific/Saipan', 'Pacific/Saipan'), ('Pacific/Tahiti', 'Pacific/Tahiti'), ('Pacific/Tarawa', 'Pacific/Tarawa'), ('Pacific/Tongatapu', 'Pacific/Tongatapu'), ('Pacific/Wake', 'Pacific/Wake'), ('Pacific/Wallis', 'Pacific/Wallis'), ('US/Alaska', 'US/Alaska'), ('US/Arizona', 'US/Arizona'), ('US/Central', 'US/Central'), ('US/Eastern', 'US/Eastern'), ('US/Hawaii', 'US/Hawaii'), ('US/Mountain', 'US/Mountain'), ('US/Pacific', 'US/Pacific'), ('UTC', 'UTC')], default='UTC', max_length=20, verbose_name='Timezone')), + ('search_player_ip', models.BooleanField(default=False, verbose_name='Include IP in Player search')), + ('show_timestamp_chat', models.BooleanField(default=False, verbose_name='Show Timestamp By Chat')), + ('last_ip', models.CharField(default='127.0.0.1', editable=False, max_length=30)), + ('auth_user', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'User Settings', + 'verbose_name_plural': 'User Settings', + }, + ), + migrations.CreateModel( + name='Warning', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('message', models.CharField(max_length=200)), + ('severity', models.CharField(choices=[('L', 'Low'), ('M', 'Medium'), ('H', 'High')], max_length=1)), + ('date', models.DateTimeField(auto_now_add=True, null=True)), + ('player', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='warning_player', to='minecraft_manager.Player')), + ('staff', models.ForeignKey(blank=True, limit_choices_to=models.Q(('rank', 'JM'), ('rank', 'MO'), ('rank', 'SM'), ('rank', 'AD'), _connector='OR'), null=True, on_delete=django.db.models.deletion.CASCADE, related_name='warning_staff', to='minecraft_manager.Player')), + ], + ), + migrations.AddField( + model_name='ip', + name='player', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='minecraft_manager.Player'), + ), + ] diff --git a/migrations/0002_auto_20180530_2223.py b/migrations/0002_auto_20180530_2223.py new file mode 100644 index 0000000..30d4309 --- /dev/null +++ b/migrations/0002_auto_20180530_2223.py @@ -0,0 +1,21 @@ +# Generated by Django 2.0.5 on 2018-05-31 03:23 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('minecraft_manager', '0001_initial'), + ] + + operations = [ + migrations.RemoveField( + model_name='player', + name='rank', + ), + migrations.RemoveField( + model_name='ticket', + name='type', + ), + ] diff --git a/migrations/0003_minecraftmanageruser.py b/migrations/0003_minecraftmanageruser.py new file mode 100644 index 0000000..085ca91 --- /dev/null +++ b/migrations/0003_minecraftmanageruser.py @@ -0,0 +1,29 @@ +# Generated by Django 2.0.5 on 2018-05-31 03:35 + +import django.contrib.auth.models +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('auth', '0009_alter_user_last_name_max_length'), + ('minecraft_manager', '0002_auto_20180530_2223'), + ] + + operations = [ + migrations.CreateModel( + name='MinecraftManagerUser', + fields=[ + ], + options={ + 'permissions': (('coreprotect_partial', 'Can use CoreProtect GUI except Command/Chat searches'), ('coreprotect_full', 'Can use full CoreProtect GUI'), ('bots', 'Can use the bot control page'), ('chat', 'Can use chat page')), + 'proxy': True, + 'indexes': [], + }, + bases=('auth.user',), + managers=[ + ('objects', django.contrib.auth.models.UserManager()), + ], + ), + ] diff --git a/migrations/0004_auto_20180530_2241.py b/migrations/0004_auto_20180530_2241.py new file mode 100644 index 0000000..bd9be5f --- /dev/null +++ b/migrations/0004_auto_20180530_2241.py @@ -0,0 +1,17 @@ +# Generated by Django 2.0.5 on 2018-05-31 03:41 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('minecraft_manager', '0003_minecraftmanageruser'), + ] + + operations = [ + migrations.AlterModelOptions( + name='minecraftmanageruser', + options={'permissions': (('coreprotect_partial', 'Can use CoreProtect GUI except Command/Chat searches'), ('coreprotect_full', 'Can use full CoreProtect GUI'), ('coreprotect_activity', 'Can use CoreProtect Activity Monitor'), ('bots', 'Can use the bot control page'), ('chat', 'Can use chat page'))}, + ), + ] diff --git a/migrations/0005_auto_20180601_2311.py b/migrations/0005_auto_20180601_2311.py new file mode 100644 index 0000000..2e401dc --- /dev/null +++ b/migrations/0005_auto_20180601_2311.py @@ -0,0 +1,20 @@ +# Generated by Django 2.0.5 on 2018-06-02 04:11 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('minecraft_manager', '0004_auto_20180530_2241'), + ] + + operations = [ + migrations.AlterField( + model_name='ticket', + name='staff', + field=models.ForeignKey(blank=True, limit_choices_to=models.Q(is_active=True), null=True, on_delete=django.db.models.deletion.CASCADE, related_name='ticket_staff', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/migrations/0006_auto_20180601_2312.py b/migrations/0006_auto_20180601_2312.py new file mode 100644 index 0000000..efc9f45 --- /dev/null +++ b/migrations/0006_auto_20180601_2312.py @@ -0,0 +1,20 @@ +# Generated by Django 2.0.5 on 2018-06-02 04:12 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('minecraft_manager', '0005_auto_20180601_2311'), + ] + + operations = [ + migrations.AlterField( + model_name='warning', + name='staff', + field=models.ForeignKey(blank=True, limit_choices_to=models.Q(is_active=True), null=True, on_delete=django.db.models.deletion.CASCADE, related_name='warning_staff', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/migrations/0007_auto_20180724_2218.py b/migrations/0007_auto_20180724_2218.py new file mode 100644 index 0000000..0811169 --- /dev/null +++ b/migrations/0007_auto_20180724_2218.py @@ -0,0 +1,18 @@ +# Generated by Django 2.0.5 on 2018-07-25 03:18 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('minecraft_manager', '0006_auto_20180601_2312'), + ] + + operations = [ + migrations.AlterField( + model_name='ticket', + name='priority', + field=models.CharField(blank=True, choices=[('L', 'Low'), ('M', 'Medium'), ('H', 'High')], default='L', max_length=1), + ), + ] diff --git a/migrations/0008_auto_20180820_1406.py b/migrations/0008_auto_20180820_1406.py new file mode 100644 index 0000000..8d9a719 --- /dev/null +++ b/migrations/0008_auto_20180820_1406.py @@ -0,0 +1,20 @@ +# Generated by Django 2.0.5 on 2018-08-20 19:06 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('minecraft_manager', '0007_auto_20180724_2218'), + ] + + operations = [ + migrations.AlterField( + model_name='ticket', + name='staff', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='ticket_staff', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/migrations/__init__.py b/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/models.py b/models.py new file mode 100644 index 0000000..6d0a43f --- /dev/null +++ b/models.py @@ -0,0 +1,438 @@ +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 +from datetime import datetime + +logger = logging.getLogger(__name__) + + +class MinecraftManagerUser(User): + + class Meta: + proxy = True + permissions = ( + ('coreprotect_partial', 'Can use CoreProtect GUI except Command/Chat searches'), + ('coreprotect_full', 'Can use full CoreProtect GUI'), + ('coreprotect_activity', 'Can use CoreProtect Activity Monitor'), + ('bots', 'Can use the bot control page'), + ('chat', 'Can use chat page'), + ) + + +class UserSettings(models.Model): + RESULT_OPTIONS = ( + (10, '10'), + (25, '25'), + (50, '50'), + (100, '100'), + (-1, 'All') + ) + THEME_OPTIONS = ( + ('DE', 'Default'), + ('DA', 'Dark'), + ('SO', 'Solar'), + ('SL', 'Slate') + ) + tzs = [] + for tz in pytz.common_timezones: + tzs.append((tz, tz)) + TIMEZONES = tuple(tzs) + 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) + 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) + + class Meta: + verbose_name = "User Settings" + verbose_name_plural = "User Settings" + + def __str__(self): + return "User Settings : %s" % self.auth_user.username + + def __repr__(self): + return self.auth_user.username + + +class UnansweredManager(models.Manager): + def get_queryset(self): + return super(UnansweredManager, self).get_queryset().filter(accepted=None) + + +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) + 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) + read_rules = models.CharField("Have you read the rules?", max_length=10) + accepted = models.NullBooleanField() + date = models.DateTimeField(auto_now_add=True, blank=True, null=True) + objects = models.Manager() + unanswered = UnansweredManager() + + @property + def status(self): + if self.accepted is not None: + if self.accepted is True: + return "Accepted" + elif self.accepted is False: + return "Denied" + else: + return "Unknown" + else: + return "Unanswered" + + @property + def date_display(self): + return str(self.date).split(".")[0] + + @property + def json(self): + return {'id': self.id, 'username': self.username, 'age': self.age, 'player_type': self.player_type, + 'ever_banned': self.ever_banned, 'ever_banned_explanation': self.ever_banned_explanation, + 'reference': self.reference, 'read_rules': self.read_rules, 'status': self.status, + 'date': self.date_display} + + @property + def skript(self): + return "{0}<:>{1}<:>{2}<:>{3}<:>{4}<:>{5}<:>{6}<:>{7}<:>{8}<:>{9}<:>{10}".format( + "success", self.id, self.username, self.age, self.player_type, self.ever_banned, + self.ever_banned_explanation, self.reference, self.read_rules, self.status, self.date_display + ) + + def __str__(self): + return "Application: %s" % self.username + + def __repr__(self): + return self.username + + +class Player(models.Model): + auth_user = models.OneToOneField(User, on_delete=models.DO_NOTHING, null=True, blank=True) + uuid = models.CharField(max_length=36, unique=True) + username = models.CharField(max_length=20) + application = models.ForeignKey(Application, on_delete=models.CASCADE, null=True, blank=True) + first_seen = models.DateField(null=True, blank=True) + last_seen = models.DateField(null=True, blank=True) + + + @property + def is_banned(self): + ban_file = os.path.join(settings.MINECRAFT_BASE_DIR, 'banned-players.json') + with open(ban_file, encoding='utf-8') as f: + bans = json.load(f) + if bans: + for ban in bans: + if self.uuid == ban['uuid']: + return True + return False + else: + return False + + @property + def donor_status(self): + + def format_time(timestamp): + return datetime.utcfromtimestamp(timestamp).strftime('%m/%d/%Y') + + try: + from django_luckperms.models import LuckPermsPlayerPermissions as lppp + donator = lppp.objects.filter(permission='group.donator', uuid=self.uuid) + if donator.exists(): + expiry = donator.first().expiry + if expiry > 0: + return "Until {}".format(format_time(expiry)) + else: + return "Permanent" + else: + return "None" + except: + pass + try: + pex_file = os.path.join(getattr(settings, 'MINECRAFT_BASE_DIR', ''), 'plugins/PermissionsEx/permissions.yml') + with open(pex_file) as f: + yml = yaml.load(f) + for user in yml['users']: + if user == self.uuid: + if 'donator' in yml['users'][user]['group']: + u = yml['users'][user] + if 'group-donator-until' in u['options']: + unix_time = float(u['options']['group-donator-until']) + return 'Until {0}'.format(format_time(unix_time)) + else: + return 'Permanent' + else: + return 'None' + except: + pass + return "N/A" + + @property + def ips(self): + ips = [] + query = IP.objects.filter(player=self) + for q in query: + ips.append(q.ip) + return " ".join(ips) + + def __str__(self): + return self.username + " (" + self.uuid + ")" + + +class UnclaimedManager(models.Manager): + def get_queryset(self): + return super(UnclaimedManager, self).get_queryset().filter(staff=None, resolved=False) + + +class ClaimedManager(models.Manager): + def get_queryset(self): + return super(ClaimedManager, self).get_queryset().filter(staff__isnull=False, resolved=False) + + +class Ticket(models.Model): + PRIORITY = ( + ('L', 'Low'), + ('M', 'Medium'), + ('H', 'High') + ) + WORLDS = ( + ('O', 'Overworld'), + ('N', 'Nether'), + ('E', 'The End') + ) + player = models.ForeignKey(Player, related_name="ticket_player", on_delete=models.CASCADE, blank=True, null=True) + message = models.CharField(max_length=500) + priority = models.CharField(max_length=1, choices=PRIORITY, blank=True, default='L') + staff = models.ForeignKey(User, related_name="ticket_staff", on_delete=models.CASCADE, null=True, blank=True) + resolved = models.BooleanField(default=False) + world = models.CharField(max_length=1, choices=WORLDS, blank=True, null=True) + x = models.CharField(max_length=20, blank=True, null=True) + y = models.CharField(max_length=20, blank=True, null=True) + z = models.CharField(max_length=20, blank=True, null=True) + date = models.DateTimeField(auto_now_add=True, null=True, blank=True) + objects = models.Manager() + unclaimed = UnclaimedManager() + claimed = ClaimedManager() + + @property + def location(self): + return "{0}, {1}, {2} in {3}".format(self.x, self.y, self.z, self.world_label) + + @property + def world_label(self): + for W in self.WORLDS: + if W[0] == self.world: + return W[1] + return "" + + @property + def resolved_label(self): + if self.resolved: + return "Resolved" + else: + return "Unresolved" + + @property + def snippet(self): + if len(self.message) > 50: + return self.message[:50] + "..." + else: + return self.message + + @property + def issuer(self): + if self.player: + pl = Player.objects.get(id=self.player_id) + return pl.username + else: + return "Unknown" + + @property + def claimed_by(self): + if self.staff: + pl = User.objects.get(id=self.staff_id) + return pl.username + else: + return "Unclaimed" + + @property + def is_claimed(self): + if self.staff: + return True + else: + return False + + @property + def priority_display(self): + for P in self.PRIORITY: + if P[0] == self.priority: + return P[1] + return "Unknown" + + @staticmethod + def priority_code_to_display(piority_code): + for P in Ticket.PRIORITY: + if P[0] == piority_code: + return P[1] + return "Unknown" + + @property + def date_display(self): + return str(self.date).split(".")[0] + + def __str__(self): + username = "Unknown" + try: + pl = Player.objects.get(id=self.player_id) + username = pl.username + except: + pass + return "Ticket from %s" % username + + +class Warning(models.Model): + SEVERITY = ( + ('L', 'Low'), + ('M', 'Medium'), + ('H', 'High') + ) + player = models.ForeignKey(Player, related_name="warning_player", on_delete=models.CASCADE) + message = models.CharField(max_length=200) + severity = models.CharField(max_length=1, choices=SEVERITY) + staff = models.ForeignKey(User, related_name="warning_staff", on_delete=models.CASCADE, null=True, blank=True, limit_choices_to=Q(is_active=True)) + date = models.DateTimeField(auto_now_add=True, blank=True, null=True) + + @property + def snippet(self): + if len(self.message) > 50: + return self.message[:50] + "..." + else: + return self.message + + @property + def severity_display(self): + for S in self.SEVERITY: + if S[0] == self.severity: + return S[1] + return "Unknown" + + @staticmethod + def severity_code_to_display(severity_code): + for S in Warning.SEVERITY: + if S[0] == severity_code: + return S[1] + return "Unknown" + + @staticmethod + def resolve(severity): + for S in Warning.SEVERITY: + if severity.lower() in (S[0].lower(), S[1].lower()): + return S[0] + return 'L' + + @property + def issuer(self): + if self.staff: + pl = User.objects.get(id=self.staff_id) + return pl.username + else: + return "System" + + @property + def issuee(self): + if self.player: + pl = Player.objects.get(id=self.player_id) + return pl.username + else: + return "Unknown" + + @property + def date_display(self): + return str(self.date).split(".")[0] + + def __str__(self): + username = "Unknown" + try: + pl = Player.objects.get(id=self.player_id) + username = pl.username + except: + pass + return "Warning for %s" % username + + +class IP(models.Model): + player = models.ForeignKey(Player, on_delete=models.CASCADE) + ip = models.CharField(max_length=30) + + class Meta: + verbose_name = "IP" + verbose_name_plural = "IPs" + + @property + def associated(self): + ips = IP.objects.filter(ip=self.ip) + players = [] + for ip in ips: + if self.player != ip.player: + players.append(ip.player) + if players: + return players + else: + return None + + def __str__(self): + player = Player.objects.get(id=self.player_id) + return player.username + " - " + self.ip + + +class Alert(models.Model): + user = models.ForeignKey(User, on_delete=models.CASCADE) + message = models.TextField(max_length=1000) + seen = models.BooleanField(default=False) + date = models.DateTimeField(auto_now_add=True, blank=True, null=True) + + @property + def snippet(self): + if len(self.message) > 50: + return self.message[:50] + "..." + else: + return self.message + + def __str__(self): + return "Alert for %s" % self.user.username + + +class Note(models.Model): + NOTABLE = ( + ('PL', 'Player'), + ('TI', 'Ticket'), + ('WA', 'Warning') + ) + author = models.ForeignKey(User, on_delete=models.CASCADE) + ref_table = models.CharField(max_length=2, choices=NOTABLE) + ref_id = models.CharField(max_length=4) + message = models.TextField(max_length=1000) + 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 ref_model(self): + if self.ref_table == 'PL': + return Player + elif self.ref_table == 'TI': + return Ticket + elif self.ref_table == 'WA': + return Warning + + def __str__(self): + ref = self.ref_model.objects.get(id=self.ref_id) + return "Note: {0}".format(ref) + + + diff --git a/static/FilterByText.js b/static/FilterByText.js new file mode 100644 index 0000000..54ae038 --- /dev/null +++ b/static/FilterByText.js @@ -0,0 +1,25 @@ +jQuery.fn.filterByText = function(textbox) { + return this.each(function() { + var select = this; + var options = []; + $(select).find('option').each(function() { + options.push({value: $(this).val(), text: $(this).text()}); + }); + $(select).data('options', options); + + $(textbox).bind('change keyup', function() { + var options = $(select).empty().data('options'); + var search = $.trim($(this).val()); + var regex = new RegExp(search,"gi"); + + $.each(options, function(i) { + var option = options[i]; + if(option.text.match(regex) !== null) { + $(select).append( + $('