From cfeabc37274a79f76f484e857b2a93d836e629f3 Mon Sep 17 00:00:00 2001 From: Etzelia Date: Sat, 20 Oct 2018 22:59:10 -0500 Subject: [PATCH 01/20] Added demote command to bot Updated Docs Fixes #12 --- api/api.py | 1 + api/bot.py | 19 +++++++++++++++++-- docs/source/getting-started.rst | 1 + 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/api/api.py b/api/api.py index 8127661..411235f 100644 --- a/api/api.py +++ b/api/api.py @@ -18,6 +18,7 @@ PLUGIN_ACCEPT = 'accept' PLUGIN_DENY = 'deny' PLUGIN_GLOBAL_CHAT = 'global' PLUGIN_STAFF_CHAT = 'staff' +PLUGIN_DEMOTE = 'demote' def plugin(key, command): diff --git a/api/bot.py b/api/bot.py index 942bbc3..6febf6b 100644 --- a/api/bot.py +++ b/api/bot.py @@ -1,6 +1,7 @@ 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.contrib.auth.models import User from django.conf import settings from django.db import close_old_connections from threading import Thread @@ -67,7 +68,8 @@ class Discord(discord.Client): 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") + embed.add_field(name="{}demote ".format(self.prefix), value="Demote a player to first accepted role.") + 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) @@ -103,7 +105,7 @@ class Discord(discord.Client): 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)) + 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: @@ -118,6 +120,19 @@ class Discord(discord.Client): else: info = "No applications matched that search." yield from self.discord_message(message.channel, info) + # DEMOTE A PLAYER TO MEMBER + match = re.match("[{0}]demote (\w+)$".format(self.prefix), message.content) + if match: + yield from self.delete_message(message) + username = match.group(1) + api.plugin(api.PLUGIN_DEMOTE, username) + deactivated = "" + if User.objects.filter(username__iexact=username).exists(): + user = User.objects.get(username__iexact=username) + user.is_active = False + user.save() + deactivated = " and de-activated" + yield from self.discord_message(message.channel, "{} has been demoted{}.".format(username, deactivated)) # COMPARE DISCORD USERS TO WHITELIST match = re.match("[{0}]compare".format(self.prefix), message.content) if match: diff --git a/docs/source/getting-started.rst b/docs/source/getting-started.rst index d24cb3d..72a6031 100644 --- a/docs/source/getting-started.rst +++ b/docs/source/getting-started.rst @@ -47,6 +47,7 @@ Add MCM urls to your ``urls.py`` Django doesn't provide login/logout templates by default, so MCM has some generic ones if needed. :: + from django.contrib.auth import views as auth_views path('accounts/login/', auth_views.LoginView.as_view(template_name='minecraft_manager/login.html'), name='login'), path('accounts/logout/', auth_views.LogoutView.as_view(template_name='minecraft_manager/logged_out.html'), name='logout'), From d35b77014feb9b559281bb981bd53f7bd0cb6b50 Mon Sep 17 00:00:00 2001 From: Etzelia Date: Sun, 21 Oct 2018 16:40:47 -0500 Subject: [PATCH 02/20] Added support for filtering at the individual stat level --- external/stats.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/external/stats.py b/external/stats.py index 94b8764..dad52c6 100644 --- a/external/stats.py +++ b/external/stats.py @@ -1,4 +1,4 @@ -import os, json +import os, json, copy from minecraft_manager.models import Player from django.conf import settings @@ -16,9 +16,13 @@ def get_stats(): with open(stats_dir + "/" + filename) as json_file: raw = json.load(json_file)['stats'] clean = {} - for r in raw: - if not any(sf.lower() in r.lower() for sf in stats_filter): - clean[r] = raw[r] + raw_copy = copy.deepcopy(raw) + for ra in raw_copy: + if not any(sf.lower() in ra.lower() for sf in stats_filter): + for r in raw_copy[ra]: + if any(sf.lower() in r.lower() for sf in stats_filter): + del raw[ra][r] + clean[ra] = raw[ra] uuid = filename.replace(".json", "") stats[uuid] = clean return stats From 7ae8fa576da093d1c206f7a9106f14120c7170fc Mon Sep 17 00:00:00 2001 From: Etzelia Date: Wed, 21 Nov 2018 17:04:47 -0600 Subject: [PATCH 03/20] Started work on API tokens --- api/admin.py | 41 +++++++++++++++++++++++++++++++++++++++++ api/models.py | 16 ++++++++++++++++ api/views.py | 32 ++++++++++++++++---------------- 3 files changed, 73 insertions(+), 16 deletions(-) create mode 100644 api/admin.py create mode 100644 api/models.py diff --git a/api/admin.py b/api/admin.py new file mode 100644 index 0000000..51fced1 --- /dev/null +++ b/api/admin.py @@ -0,0 +1,41 @@ +from django.contrib import admin +from django.utils.translation import ugettext_lazy as _ +from minecraft_manager.api.models import Token + + +class TokenActiveFilter(admin.SimpleListFilter): + title = _('Active') + parameter_name = 'active' + + def lookups(self, request, model_admin): + return ( + ('0', _('Active')), + ('1', _('Inactive')), + ) + + def queryset(self, request, queryset): + if self.value() == '0': + return queryset.filter(active=True) + if self.value() == '1': + return queryset.filter(active=False) + + +class TokenAdmin(admin.ModelAdmin): + list_filter = (TokenActiveFilter,) + fieldsets = ( + (None, { + 'fields': ('key', 'active') + }), + ('Permissions', { + 'fields': ('web_get_permission', 'web_post_permission', 'plugin_get_permission', 'plugin_post_permission', + 'form_get_permission', 'form_post_permission', 'model_get_permission', 'model_post_permission', + 'stats_get_permission', 'stats_post_permission') + }) + ) + + +try: + admin.site.register(Token, TokenAdmin) +except admin.sites.AlreadyRegistered: + pass + diff --git a/api/models.py b/api/models.py new file mode 100644 index 0000000..6dbdb29 --- /dev/null +++ b/api/models.py @@ -0,0 +1,16 @@ +from django.db import models + + +class Token(models.Model): + key = models.CharField("Key", max_length=50, unique=True) + active = models.BooleanField("Active", default=True) + web_get_permission = models.BooleanField("Web API GET", default=False) + web_post_permission = models.BooleanField("Web API POST", default=False) + plugin_get_permission = models.BooleanField("Plugin API GET", default=False) + plugin_post_permission = models.BooleanField("Plugin API POST", default=False) + form_get_permission = models.BooleanField("Form API GET", default=False) + form_post_permission = models.BooleanField("Form API POST", default=False) + model_get_permission = models.BooleanField("Model API GET", default=False) + model_post_permission = models.BooleanField("Model API POST", default=False) + stats_get_permission = models.BooleanField("Stats API GET", default=False) + stats_post_permission = models.BooleanField("Stats API POST", default=False) diff --git a/api/views.py b/api/views.py index 7d60377..603da56 100644 --- a/api/views.py +++ b/api/views.py @@ -7,7 +7,6 @@ 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 @@ -15,29 +14,30 @@ 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 +from minecraft_manager.api.models import Token import minecraft_manager.utils as mcm_utils import minecraft_manager.external.stats as mcm_stats logger = logging.getLogger(__name__) -def request_allowed(request): +def request_allowed(request, permission): 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 + 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 + token_permission = False + if Token.objects.filter(active=True, key=request_password).exists(): + token = Token.objects.get(active=True, key=request_password) + token_permission = getattr(token, permission, False) + return is_authenticated or token_permission def clean(model, data): @@ -60,7 +60,7 @@ class WebAPI(View): def get(self, request, keyword): get = request.GET data = {'success': False, 'message': 'API failed'} - if request_allowed(request): + if request_allowed(request, 'web_get_permission'): keyword = keyword.lower() if keyword == 'log': html_global = "" @@ -102,7 +102,7 @@ class WebAPI(View): def post(self, request, keyword): post = request.POST data = {} - if request_allowed(request): + if request_allowed(request, 'web_post_permission'): 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))]: @@ -155,7 +155,7 @@ class PluginAPI(View): def get(self, request, keyword): json = {'status': True, 'message': '', 'extra': ''} - if request_allowed(request): + if request_allowed(request, 'plugin_get_permission'): get = request.GET keyword = keyword.lower() @@ -163,7 +163,7 @@ class PluginAPI(View): def post(self, request, keyword): json = {'status': True, 'message': '', 'extra': ''} - if request_allowed(request): + if request_allowed(request, 'plugin_post_permission'): post = request.POST keyword = keyword.lower() if "application" == keyword: @@ -323,7 +323,7 @@ class FormAPI(View): def get(self, request, request_model): html = "" - if request_allowed(request): + if request_allowed(request, 'form_get_permission'): get = request.GET model = None for m in apps.get_app_config('minecraft_manager').get_models(): @@ -346,7 +346,7 @@ class FormAPI(View): def post(self, request, request_model): html = "" - if request_allowed(request): + if request_allowed(request, 'form_post_permission'): post = request.POST model = None for m in apps.get_app_config('minecraft_manager').get_models(): @@ -376,7 +376,7 @@ class ModelAPI(View): def get(self, request, request_model): json = [] - if request_allowed(request): + if request_allowed(request, 'model_get_permission'): get = request.GET model = None for m in apps.get_app_config('minecraft_manager').get_models(): @@ -404,7 +404,7 @@ class StatsAPI(View): def get(self, request): json = [] - if request_allowed(request): + if request_allowed(request, 'stats_get_permission'): get = request.GET if 'stat' in get: if 'uuid' in get: From 20c97188ef20ef67a4dff36dbf4e135a3aa6734c Mon Sep 17 00:00:00 2001 From: Etzelia Date: Wed, 21 Nov 2018 21:15:21 -0600 Subject: [PATCH 04/20] Finished API tokens --- admin.py | 2 ++ api/admin.py | 12 ++++++++---- api/api.py | 8 ++++++-- api/models.py | 26 +++++++++++++++----------- api/views.py | 16 ++++++---------- 5 files changed, 37 insertions(+), 27 deletions(-) diff --git a/admin.py b/admin.py index a06e53e..e01ee22 100644 --- a/admin.py +++ b/admin.py @@ -5,6 +5,7 @@ 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 +from minecraft_manager.api.admin import register as api_register class PlayerInline(admin.StackedInline): @@ -100,6 +101,7 @@ try: admin.site.register(IP, IPAdmin) admin.site.register(Alert) admin.site.register(Note) + api_register() except admin.sites.AlreadyRegistered: pass diff --git a/api/admin.py b/api/admin.py index 51fced1..99044c7 100644 --- a/api/admin.py +++ b/api/admin.py @@ -22,6 +22,9 @@ class TokenActiveFilter(admin.SimpleListFilter): class TokenAdmin(admin.ModelAdmin): list_filter = (TokenActiveFilter,) + list_display = ('key', 'active', 'web_get_permission', 'web_post_permission', 'plugin_get_permission', + 'plugin_post_permission', 'form_get_permission', 'form_post_permission', 'model_get_permission', + 'model_post_permission', 'stats_get_permission', 'stats_post_permission') fieldsets = ( (None, { 'fields': ('key', 'active') @@ -34,8 +37,9 @@ class TokenAdmin(admin.ModelAdmin): ) -try: - admin.site.register(Token, TokenAdmin) -except admin.sites.AlreadyRegistered: - pass +def register(): + try: + admin.site.register(Token, TokenAdmin) + except admin.sites.AlreadyRegistered: + pass diff --git a/api/api.py b/api/api.py index 411235f..eadc7dc 100644 --- a/api/api.py +++ b/api/api.py @@ -1,4 +1,4 @@ -import socket, requests, logging, os, datetime, pytz, mcstatus, discord +import socket, requests, logging, os, datetime, pytz, mcstatus, random, string from minecraft_manager.models import Alert from django.contrib.auth.models import User from django.conf import settings @@ -140,4 +140,8 @@ def get_query(): 'players': sorted(query.players.names)} except: return {'max': 0, 'online': 0, - 'players': []} \ No newline at end of file + 'players': []} + + +def generate_password(size=20): + return "".join([random.choice(string.ascii_letters + string.digits) for idx in range(0, size)]) \ No newline at end of file diff --git a/api/models.py b/api/models.py index 6dbdb29..e70bddf 100644 --- a/api/models.py +++ b/api/models.py @@ -1,16 +1,20 @@ from django.db import models +from minecraft_manager.api.api import generate_password class Token(models.Model): - key = models.CharField("Key", max_length=50, unique=True) + key = models.CharField("Key", default=generate_password, max_length=50, unique=True) active = models.BooleanField("Active", default=True) - web_get_permission = models.BooleanField("Web API GET", default=False) - web_post_permission = models.BooleanField("Web API POST", default=False) - plugin_get_permission = models.BooleanField("Plugin API GET", default=False) - plugin_post_permission = models.BooleanField("Plugin API POST", default=False) - form_get_permission = models.BooleanField("Form API GET", default=False) - form_post_permission = models.BooleanField("Form API POST", default=False) - model_get_permission = models.BooleanField("Model API GET", default=False) - model_post_permission = models.BooleanField("Model API POST", default=False) - stats_get_permission = models.BooleanField("Stats API GET", default=False) - stats_post_permission = models.BooleanField("Stats API POST", default=False) + web_get_permission = models.BooleanField("Web GET", default=False) + web_post_permission = models.BooleanField("Web POST", default=False) + plugin_get_permission = models.BooleanField("Plugin GET", default=False) + plugin_post_permission = models.BooleanField("Plugin POST", default=False) + form_get_permission = models.BooleanField("Form GET", default=False) + form_post_permission = models.BooleanField("Form POST", default=False) + model_get_permission = models.BooleanField("Model GET", default=False) + model_post_permission = models.BooleanField("Model POST", default=False) + stats_get_permission = models.BooleanField("Stats GET", default=False) + stats_post_permission = models.BooleanField("Stats POST", default=False) + + def __str__(self): + return self.key diff --git a/api/views.py b/api/views.py index 603da56..26094ba 100644 --- a/api/views.py +++ b/api/views.py @@ -1,6 +1,6 @@ from __future__ import absolute_import -import logging, random, string, datetime +import logging, datetime from django.contrib.auth.forms import PasswordChangeForm from django.contrib.auth import update_session_auth_hash from django.apps import apps @@ -11,7 +11,7 @@ from django.utils import timezone from django.views.generic import View from django.forms import modelform_factory -import minecraft_manager.forms as MCMForms +import minecraft_manager.forms as mcm_forms from minecraft_manager.models import Player, UserSettings, Application, IP, Ticket, Warning import minecraft_manager.api.api as mcm_api from minecraft_manager.api.models import Token @@ -51,10 +51,6 @@ def clean(model, data): 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): @@ -119,7 +115,7 @@ class WebAPI(View): else: return HttpResponse(form.as_p()) elif keyword == 'alert': - form = MCMForms.AlertForm(request.POST) + form = mcm_forms.AlertForm(request.POST) if form.is_valid(): if mcm_api.create_alert(form.cleaned_data['message']): data = {'success': True} @@ -284,7 +280,7 @@ class PluginAPI(View): json['status'] = False json['message'] = "You are already registered. To change your password, contact an Admin." else: - password = generate_password() + password = mcm_api.generate_password() user = User.objects.create_user(username=player.username.lower(), password=password) user.save() player.auth_user = user @@ -332,7 +328,7 @@ class FormAPI(View): break if model: form = None - for modelform in MCMForms.__all__(): + for modelform in mcm_forms.__all__(): if modelform.Meta.model == model: form = modelform() break @@ -355,7 +351,7 @@ class FormAPI(View): break if model: form = None - for modelform in MCMForms.__all__(): + for modelform in mcm_forms.__all__(): if modelform.Meta.model == model: form = modelform(post) break From 73a6c51d441c113b2a99c8c20d8f8c007c008e67 Mon Sep 17 00:00:00 2001 From: Etzelia Date: Wed, 21 Nov 2018 21:17:23 -0600 Subject: [PATCH 05/20] Updated docs --- docs/source/django-settings.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/source/django-settings.rst b/docs/source/django-settings.rst index 387b2b5..495131d 100644 --- a/docs/source/django-settings.rst +++ b/docs/source/django-settings.rst @@ -47,8 +47,6 @@ Optional ``SERVER_QUERY_IP`` - The full IP (and port) used to query your server. (This is used to get a player list) -``API_PASSWORD`` - The password used to validate API requests from unauthenticated sources. - ``COREPROTECT_WEB_URL`` - The URL to your CoreProtect Web UI, if it exists. ``COREPROTECT_ACTIVITY_URL`` - The URL to your CoreProtect Activity Web UI, if it exists. From 6e1063bb0e9022775032ea7780286acea04ca954 Mon Sep 17 00:00:00 2001 From: Etzelia Date: Fri, 23 Nov 2018 16:28:05 +0100 Subject: [PATCH 06/20] Add description field to Tokens --- api/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/api/models.py b/api/models.py index e70bddf..9ac1319 100644 --- a/api/models.py +++ b/api/models.py @@ -4,6 +4,7 @@ from minecraft_manager.api.api import generate_password class Token(models.Model): key = models.CharField("Key", default=generate_password, max_length=50, unique=True) + description = models.CharField("Description", max_length=200, blank=True) active = models.BooleanField("Active", default=True) web_get_permission = models.BooleanField("Web GET", default=False) web_post_permission = models.BooleanField("Web POST", default=False) From d7a23cfcbcd05ee0a53e778270c35fba9d4bf8d9 Mon Sep 17 00:00:00 2001 From: Etzelia Date: Fri, 23 Nov 2018 16:30:45 +0100 Subject: [PATCH 07/20] Add description field to admin page. --- api/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/admin.py b/api/admin.py index 99044c7..7c3bec2 100644 --- a/api/admin.py +++ b/api/admin.py @@ -27,7 +27,7 @@ class TokenAdmin(admin.ModelAdmin): 'model_post_permission', 'stats_get_permission', 'stats_post_permission') fieldsets = ( (None, { - 'fields': ('key', 'active') + 'fields': ('key', 'active', 'description') }), ('Permissions', { 'fields': ('web_get_permission', 'web_post_permission', 'plugin_get_permission', 'plugin_post_permission', From c16c4806207ac310a36c693148ccbad65a94d832 Mon Sep 17 00:00:00 2001 From: Etzelia Date: Fri, 23 Nov 2018 16:33:06 +0100 Subject: [PATCH 08/20] Replace key with description on admin list page. --- api/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/admin.py b/api/admin.py index 7c3bec2..ac7579f 100644 --- a/api/admin.py +++ b/api/admin.py @@ -22,7 +22,7 @@ class TokenActiveFilter(admin.SimpleListFilter): class TokenAdmin(admin.ModelAdmin): list_filter = (TokenActiveFilter,) - list_display = ('key', 'active', 'web_get_permission', 'web_post_permission', 'plugin_get_permission', + list_display = ('description', 'active', 'web_get_permission', 'web_post_permission', 'plugin_get_permission', 'plugin_post_permission', 'form_get_permission', 'form_post_permission', 'model_get_permission', 'model_post_permission', 'stats_get_permission', 'stats_post_permission') fieldsets = ( From 4cd9d129142db39c3d819cc50b5b831abf7af255 Mon Sep 17 00:00:00 2001 From: Etzelia Date: Fri, 23 Nov 2018 09:38:37 -0600 Subject: [PATCH 09/20] Added display property to Token to make admin page more intuitive. --- api/admin.py | 2 +- api/models.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/api/admin.py b/api/admin.py index ac7579f..4a89974 100644 --- a/api/admin.py +++ b/api/admin.py @@ -22,7 +22,7 @@ class TokenActiveFilter(admin.SimpleListFilter): class TokenAdmin(admin.ModelAdmin): list_filter = (TokenActiveFilter,) - list_display = ('description', 'active', 'web_get_permission', 'web_post_permission', 'plugin_get_permission', + list_display = ('display', 'active', 'web_get_permission', 'web_post_permission', 'plugin_get_permission', 'plugin_post_permission', 'form_get_permission', 'form_post_permission', 'model_get_permission', 'model_post_permission', 'stats_get_permission', 'stats_post_permission') fieldsets = ( diff --git a/api/models.py b/api/models.py index 9ac1319..28948d8 100644 --- a/api/models.py +++ b/api/models.py @@ -17,5 +17,9 @@ class Token(models.Model): stats_get_permission = models.BooleanField("Stats GET", default=False) stats_post_permission = models.BooleanField("Stats POST", default=False) + @property + def display(self): + return self.description if self.description else self.key + def __str__(self): return self.key From c9d55463864a9a739842a57521090a02cb4f519c Mon Sep 17 00:00:00 2001 From: Etzelia Date: Wed, 5 Dec 2018 19:16:36 +0100 Subject: [PATCH 10/20] Banned IP Match Only ping once with the name of all associated banned players. --- api/views.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/api/views.py b/api/views.py index 26094ba..59c1657 100644 --- a/api/views.py +++ b/api/views.py @@ -268,10 +268,13 @@ class PluginAPI(View): player.last_seen = timezone.now().strftime("%Y-%m-%d") player.save() if new_player and ip.associated: + 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) + associated.append(assoc) + if associated: + mcm_api.plugin("staff", "Server {0}'s IP matches the banned player(s) {1}".format(player.username, ", ".join([assoc.username for assoc in associated]))) + mcm_api.discord_notification("{0}'s IP matches the banned player(s) {1}".format(player.username, ", ".join([assoc.username for assoc in associated])), ping=True) json['status'] = True json['message'] = "Updated {0}".format(post['username']) elif "register" == keyword: From 611c4e19348b1a7e6b85b0ebb36c4ed493fb9686 Mon Sep 17 00:00:00 2001 From: Etzelia Date: Wed, 5 Dec 2018 16:58:26 -0600 Subject: [PATCH 11/20] First pass on overview Need to add more and maybe make it look better... --- overview.py | 52 +++++++ templates/minecraft_manager/overview.html | 170 +++++----------------- views.py | 17 +-- 3 files changed, 93 insertions(+), 146 deletions(-) create mode 100644 overview.py diff --git a/overview.py b/overview.py new file mode 100644 index 0000000..ab1687c --- /dev/null +++ b/overview.py @@ -0,0 +1,52 @@ +import os +import json +from django.conf import settings +from minecraft_manager.models import Application, Player, Ticket, Warning, IP + + +def overview_data(): + data = {} + + # Setup + with open(os.path.join(settings.MINECRAFT_BASE_DIR, 'banned-players.json'), encoding='utf-8') as f: + bans = json.load(f) + + + # Totals + data['total'] = { + 'application': { + 'accepted': Application.objects.filter(accepted=True).count(), + 'denied': Application.objects.filter(accepted=False).count(), + 'all': Application.objects.count() + }, + 'player': { + 'banned': len(bans), + 'unbanned': Player.objects.count() - len(bans), + 'all': Player.objects.count() + }, + 'ticket': { + 'claimed': Ticket.objects.filter(staff__isnull=False).count(), + 'unclaimed': Ticket.objects.filter(staff__isnull=True).count(), + 'resolved': Ticket.objects.filter(resolved=True).count(), + 'unresolved': Ticket.objects.filter(resolved=False).count(), + 'all': Ticket.objects.count() + }, + 'warning': Warning.objects.count(), + 'ip': IP.objects.count() + } + + # Averages + data['average'] = { + 'age': sum([application.age for application in Application.objects.all()]) / + data['total']['application']['all'] if data['total']['application']['all'] != 0 else 1 + } + + # Ratios + data['ratio'] = { + 'accepted': data['total']['application']['accepted'] / + data['total']['application']['denied'] if data['total']['application']['denied'] != 0 else 1, + 'banned': data['total']['player']['banned'] / + data['total']['player']['unbanned'] if data['total']['player']['unbanned'] != 0 else 1 + } + + return data diff --git a/templates/minecraft_manager/overview.html b/templates/minecraft_manager/overview.html index 5cd2646..63f261b 100644 --- a/templates/minecraft_manager/overview.html +++ b/templates/minecraft_manager/overview.html @@ -6,141 +6,45 @@ {% endblock %} {% block section %}
-
-
-

Applications{% if form.apps.unanswered > 0 %} -

- +
+
+

Applications: {{ data.total.application.all }}

+

Accepted: {{ data.total.application.accepted }}

+

Denied: {{ data.total.application.denied }}

+
+

Tickets: {{ data.total.ticket.all }}

+

Claimed: {{ data.total.ticket.claimed }}

+

Unclaimed: {{ data.total.ticket.unclaimed }}

+

Resolved: {{ data.total.ticket.resolved }}

+

Unresolved: {{ data.total.ticket.unresolved }}

-
-
-

Totals

- -
+
+

Players: {{ data.total.player.all }}

+

Not Banned: {{ data.total.player.unbanned }}

+

Banned: {{ data.total.player.banned }}

+
+

Warnings: {{ data.total.warning }}

+
+

IPs: {{ data.total.ip }}

- +
+
+
+
+
+

Acceptance Rate: {{ data.ratio.accepted }}

+
+
+

Ban Rate: {{ data.ratio.banned }}

+
+
+
{% endblock section %} \ No newline at end of file diff --git a/views.py b/views.py index d2769e9..46be592 100644 --- a/views.py +++ b/views.py @@ -14,6 +14,7 @@ from django.views.generic import View from django.contrib.auth.models import User from minecraft_manager.models import Application as AppModel, Player as PlayerModel, Ticket as TicketModel, Warning as WarningModel, IP as IPModel, Alert as AlertModel, Note as NoteModel, UserSettings as UserSettingsModel from minecraft_manager.forms import WarningForm, NoteForm +from minecraft_manager.overview import overview_data import minecraft_manager.api.api as API import subprocess @@ -33,19 +34,9 @@ class Overview(View): request.user.usersettings = UserSettingsModel(auth_user=request.user) request.user.usersettings.last_ip = user_ip request.user.usersettings.save() - unanswered_apps = AppModel.objects.filter(accepted=None).count() - accepted_apps = AppModel.objects.filter(accepted=True).count() - denied_apps = AppModel.objects.filter(accepted=False).count() - unclaimed_tickets = TicketModel.objects.filter(staff=None, resolved=False).count() - claimed_tickets = TicketModel.objects.filter(staff__isnull=False, resolved=False).count() - resolved_tickets = TicketModel.objects.filter(resolved=True).count() - counts = {'applications': AppModel.objects.count(), 'players': PlayerModel.objects.count(), - "ips": IPModel.objects.count(), "tickets": TicketModel.objects.count(), - "warnings": WarningModel.objects.count()} - form = {'apps': {'unanswered': unanswered_apps, 'accepted': accepted_apps, 'denied': denied_apps}, - 'tickets': {'unclaimed': unclaimed_tickets, 'claimed': claimed_tickets, 'resolved': resolved_tickets}, - 'counts': counts} - return render(request, 'minecraft_manager/overview.html', {'current_app': 'overview', 'form': form}) + + + return render(request, 'minecraft_manager/overview.html', {'current_app': 'overview', 'data': overview_data()}) class CoreProtect(View): From 9f3d7e7ae3aab6494ae11d23dd6ec517f5da74fa Mon Sep 17 00:00:00 2001 From: Etzelia Date: Wed, 5 Dec 2018 17:01:50 -0600 Subject: [PATCH 12/20] Changed ratios to percentages --- overview.py | 8 ++++---- templates/minecraft_manager/overview.html | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/overview.py b/overview.py index ab1687c..b8ae322 100644 --- a/overview.py +++ b/overview.py @@ -43,10 +43,10 @@ def overview_data(): # Ratios data['ratio'] = { - 'accepted': data['total']['application']['accepted'] / - data['total']['application']['denied'] if data['total']['application']['denied'] != 0 else 1, - 'banned': data['total']['player']['banned'] / - data['total']['player']['unbanned'] if data['total']['player']['unbanned'] != 0 else 1 + 'accepted': (data['total']['application']['accepted'] / + data['total']['application']['denied'] if data['total']['application']['denied'] != 0 else 1) * 100, + 'banned': (data['total']['player']['banned'] / + data['total']['player']['unbanned'] if data['total']['player']['unbanned'] != 0 else 1) * 100 } return data diff --git a/templates/minecraft_manager/overview.html b/templates/minecraft_manager/overview.html index 63f261b..665ac76 100644 --- a/templates/minecraft_manager/overview.html +++ b/templates/minecraft_manager/overview.html @@ -40,10 +40,10 @@
-

Acceptance Rate: {{ data.ratio.accepted }}

+

Acceptance Rate: {{ data.ratio.accepted }}%

-

Ban Rate: {{ data.ratio.banned }}

+

Ban Rate: {{ data.ratio.banned }}%

From 4570f1416a227f9a5b6d21b30a345d7d1e2d34af Mon Sep 17 00:00:00 2001 From: Etzelia Date: Wed, 5 Dec 2018 17:05:32 -0600 Subject: [PATCH 13/20] Added rounding --- overview.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/overview.py b/overview.py index b8ae322..0c7def3 100644 --- a/overview.py +++ b/overview.py @@ -43,10 +43,10 @@ def overview_data(): # Ratios data['ratio'] = { - 'accepted': (data['total']['application']['accepted'] / - data['total']['application']['denied'] if data['total']['application']['denied'] != 0 else 1) * 100, - 'banned': (data['total']['player']['banned'] / - data['total']['player']['unbanned'] if data['total']['player']['unbanned'] != 0 else 1) * 100 + 'accepted': round((data['total']['application']['accepted'] / + data['total']['application']['denied'] if data['total']['application']['denied'] != 0 else 1) * 100, 2), + 'banned': round((data['total']['player']['banned'] / + data['total']['player']['unbanned'] if data['total']['player']['unbanned'] != 0 else 1) * 100, 2) } return data From 76e6084b5b3e56a1126c96222c39fe5efbfa015a Mon Sep 17 00:00:00 2001 From: Etzelia Date: Wed, 5 Dec 2018 17:06:26 -0600 Subject: [PATCH 14/20] Forgot to round average as well --- overview.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/overview.py b/overview.py index 0c7def3..60ddcdb 100644 --- a/overview.py +++ b/overview.py @@ -37,8 +37,8 @@ def overview_data(): # Averages data['average'] = { - 'age': sum([application.age for application in Application.objects.all()]) / - data['total']['application']['all'] if data['total']['application']['all'] != 0 else 1 + 'age': round(sum([application.age for application in Application.objects.all()]) / + data['total']['application']['all'] if data['total']['application']['all'] != 0 else 1, 2) } # Ratios From b44f49239b065d29a36b7122c4fb0492c025eeac Mon Sep 17 00:00:00 2001 From: Etzelia Date: Wed, 5 Dec 2018 22:38:36 -0600 Subject: [PATCH 15/20] Overview improvements --- overview.py | 21 +++++++++++++++++---- templates/minecraft_manager/overview.html | 15 +++++++++++++-- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/overview.py b/overview.py index 60ddcdb..609ae1a 100644 --- a/overview.py +++ b/overview.py @@ -1,5 +1,6 @@ import os import json +from datetime import datetime, timedelta from django.conf import settings from minecraft_manager.models import Application, Player, Ticket, Warning, IP @@ -41,12 +42,24 @@ def overview_data(): data['total']['application']['all'] if data['total']['application']['all'] != 0 else 1, 2) } - # Ratios - data['ratio'] = { + # Percentage + data['percentage'] = { 'accepted': round((data['total']['application']['accepted'] / - data['total']['application']['denied'] if data['total']['application']['denied'] != 0 else 1) * 100, 2), + data['total']['application']['all'] if data['total']['application']['all'] != 0 else 1) * 100, 2), 'banned': round((data['total']['player']['banned'] / - data['total']['player']['unbanned'] if data['total']['player']['unbanned'] != 0 else 1) * 100, 2) + data['total']['player']['all'] if data['total']['player']['all'] != 0 else 1) * 100, 2), + 'applied': round((data['total']['application']['all'] / data['total']['player']['all']) * 100, 2) + } + + # Unique logins + now = datetime.now() + day = now - timedelta(days=1) + week = now - timedelta(weeks=1) + month = now - timedelta(weeks=4) + data['unique'] = { + 'day': Player.objects.filter(last_seen__range=[day, now]).count(), + 'week': Player.objects.filter(last_seen__range=[week, now]).count(), + 'month': Player.objects.filter(last_seen__range=[month, now]).count() } return data diff --git a/templates/minecraft_manager/overview.html b/templates/minecraft_manager/overview.html index 665ac76..24ff385 100644 --- a/templates/minecraft_manager/overview.html +++ b/templates/minecraft_manager/overview.html @@ -40,10 +40,21 @@
-

Acceptance Rate: {{ data.ratio.accepted }}%

+

Acceptance Rate: {{ data.percentage.accepted }}%

+

Application/Player Rate: {{ data.percentage.applied }}%

-

Ban Rate: {{ data.ratio.banned }}%

+

Ban Rate: {{ data.percentage.banned }}%

+
+
+
+
+
+

Logins Today: {{ data.unique.day }}

+

Logins Last Month: {{ data.unique.month }}

+
+
+

Logins Last Week: {{ data.unique.week }}

From e2aa2a035c1f4d9b6bedba279f7e467ef372f2a3 Mon Sep 17 00:00:00 2001 From: Etzelia Date: Wed, 5 Dec 2018 23:30:07 -0600 Subject: [PATCH 16/20] Overview additions Changed alert icon to a bell --- overview.py | 10 ++++++++++ templates/minecraft_manager/overview.html | 19 +++++++++++++++++++ templatetags/sidebar.py | 2 +- 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/overview.py b/overview.py index 609ae1a..3cd626b 100644 --- a/overview.py +++ b/overview.py @@ -3,6 +3,7 @@ import json from datetime import datetime, timedelta from django.conf import settings from minecraft_manager.models import Application, Player, Ticket, Warning, IP +from django.contrib.auth.models import User def overview_data(): @@ -62,4 +63,13 @@ def overview_data(): 'month': Player.objects.filter(last_seen__range=[month, now]).count() } + # Admin + data['resolved'] = [] + for user in User.objects.all().order_by('username'): + data['resolved'].append({ + 'active': user.is_active, + 'username': user.username, + 'tickets': Ticket.objects.filter(staff=user).count() + }) + return data diff --git a/templates/minecraft_manager/overview.html b/templates/minecraft_manager/overview.html index 24ff385..81fb092 100644 --- a/templates/minecraft_manager/overview.html +++ b/templates/minecraft_manager/overview.html @@ -6,6 +6,25 @@ {% endblock %} {% block section %}
+ {% if request.user.is_staff %} +
+
+

Admin Area

+
+
+

Resolved Tickets

+ {% for staff in data.resolved %} +

{% if staff.active %}Active{% else %}Inactive{% endif %} {{ staff.username }}: {{ staff.tickets }}

+ {% endfor %} +
+
+ +
+
+
+
+
+ {% endif %}

Applications: {{ data.total.application.all }}

diff --git a/templatetags/sidebar.py b/templatetags/sidebar.py index 8bae2c4..2060a43 100644 --- a/templatetags/sidebar.py +++ b/templatetags/sidebar.py @@ -17,7 +17,7 @@ def get_sidebar(current_app, request): ret = '
  •   Overview
  • '.format('class="active"' if current_app == 'overview' else "", reverse('overview')) ret += '
  •   Bans
  • '.format('class="active"' if current_app == 'ban' else '', reverse('ban')) - ret += '
  •   Alerts{}
  • '.format('class="active"' if current_app == 'alert' else '', reverse('alert'), unseen_html) + ret += '
  •   Alerts{}
  • '.format('class="active"' if current_app == 'alert' else '', reverse('alert'), unseen_html) # Models ret += '
  •   Applications
  • '.format('class="active"' if current_app == 'application' else '', reverse('application')) From 4b4419b39a408af1e18af215fe70a1ab0f39818f Mon Sep 17 00:00:00 2001 From: Etzelia Date: Wed, 5 Dec 2018 23:35:16 -0600 Subject: [PATCH 17/20] Squashed admin area --- templates/minecraft_manager/overview.html | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/templates/minecraft_manager/overview.html b/templates/minecraft_manager/overview.html index 81fb092..0084c75 100644 --- a/templates/minecraft_manager/overview.html +++ b/templates/minecraft_manager/overview.html @@ -10,16 +10,13 @@

    Admin Area

    +

    Resolved Tickets

    -
    -

    Resolved Tickets

    - {% for staff in data.resolved %} + {% for staff in data.resolved %} +

    {% if staff.active %}Active{% else %}Inactive{% endif %} {{ staff.username }}: {{ staff.tickets }}

    - {% endfor %} -
    -
    - -
    +
    + {% endfor %}
    From 3129f8dad030674554486a4d1812ee55a8b5689c Mon Sep 17 00:00:00 2001 From: Etzelia Date: Wed, 5 Dec 2018 23:36:04 -0600 Subject: [PATCH 18/20] Too squashed --- templates/minecraft_manager/overview.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/minecraft_manager/overview.html b/templates/minecraft_manager/overview.html index 0084c75..da93c87 100644 --- a/templates/minecraft_manager/overview.html +++ b/templates/minecraft_manager/overview.html @@ -13,7 +13,7 @@

    Resolved Tickets

    {% for staff in data.resolved %} -
    +

    {% if staff.active %}Active{% else %}Inactive{% endif %} {{ staff.username }}: {{ staff.tickets }}

    {% endfor %} From 125730569bfb6a3d7d9abfdeec6b6ae33d5795f5 Mon Sep 17 00:00:00 2001 From: Etzelia Date: Thu, 6 Dec 2018 18:00:57 +0100 Subject: [PATCH 19/20] Average age only needs accepted applications --- overview.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/overview.py b/overview.py index 3cd626b..3508606 100644 --- a/overview.py +++ b/overview.py @@ -39,8 +39,8 @@ def overview_data(): # Averages data['average'] = { - 'age': round(sum([application.age for application in Application.objects.all()]) / - data['total']['application']['all'] if data['total']['application']['all'] != 0 else 1, 2) + 'age': round(sum([application.age for application in Application.objects.filter(accepted=True)]) / + data['total']['application']['accepted'] if data['total']['application']['accepted'] != 0 else 1, 2) } # Percentage From a726001145f00d8a58b4d3c15e82847295fc7b42 Mon Sep 17 00:00:00 2001 From: Etzelia Date: Thu, 6 Dec 2018 20:25:26 +0100 Subject: [PATCH 20/20] Make the demote command message longer Hopefully that forces it onto a new line? --- api/bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/bot.py b/api/bot.py index 6febf6b..9acde65 100644 --- a/api/bot.py +++ b/api/bot.py @@ -68,7 +68,7 @@ class Discord(discord.Client): 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="{}demote ".format(self.prefix), value="Demote a player to first accepted role.") + embed.add_field(name="{}demote ".format(self.prefix), value="Demote a player to the role given to accepted applications.") embed.add_field(name="{}compare".format(self.prefix), value="Compare Discord users to the Whitelist.") yield from self.discord_message(message.channel, embed) # APP COMMANDS WITH APP ID