diff --git a/allianceauth/services/modules/mumble/auth_hooks.py b/allianceauth/services/modules/mumble/auth_hooks.py index efc8df33..f1522e0a 100644 --- a/allianceauth/services/modules/mumble/auth_hooks.py +++ b/allianceauth/services/modules/mumble/auth_hooks.py @@ -1,15 +1,18 @@ import logging import urllib +from allianceauth.menu.hooks import MenuItemHook from django.conf import settings from django.template.loader import render_to_string from allianceauth.notifications import notify from allianceauth import hooks -from allianceauth.services.hooks import ServicesHook +from allianceauth.services.hooks import ServicesHook, UrlHook from .tasks import MumbleTasks from .models import MumbleUser from .urls import urlpatterns +from allianceauth.services.modules.mumble import urls +from django.utils.translation import gettext_lazy as _ logger = logging.getLogger(__name__) @@ -71,7 +74,28 @@ class MumbleService(ServicesHook): 'username': request.user.mumble.username if MumbleTasks.has_account(request.user) else '', }, request=request) - @hooks.register('services_hook') -def register_mumble_service(): +def register_mumble_service() -> ServicesHook: return MumbleService() + + +class MumbleMenuItem(MenuItemHook): + def __init__(self): + # setup menu entry for sidebar + MenuItemHook.__init__( + self=self, + text=_("Mumble Temp Links"), + classes="fa-solid fa-microphone", + url_name="mumble:index", + navactive=["mumble:index"], + ) + + def render(self, request): + if request.user.has_perm("mumble.create_new_templinks"): + return MenuItemHook.render(self, request) + return "" + + +@hooks.register("menu_item_hook") +def register_menu(): + return MumbleMenuItem() diff --git a/allianceauth/services/modules/mumble/authenticator.py b/allianceauth/services/modules/mumble/authenticator.py index ad8b2af8..579c0c03 100644 --- a/allianceauth/services/modules/mumble/authenticator.py +++ b/allianceauth/services/modules/mumble/authenticator.py @@ -430,8 +430,7 @@ def main() -> None: except (OSError, Exception) as e: logger.exception(e) - logger.debug( - f'idToTexture {id} -> image download for {avatar_url} failed, fall through') + logger.debug(f'idToTexture {id} -> image download for {avatar_url} failed, fall through') return "" # FALL_THROUGH else: file = handle.read() diff --git a/allianceauth/services/modules/mumble/managers.py b/allianceauth/services/modules/mumble/managers.py index 81185a1e..5ee02357 100644 --- a/allianceauth/services/modules/mumble/managers.py +++ b/allianceauth/services/modules/mumble/managers.py @@ -6,7 +6,7 @@ from django.db import models from allianceauth.services.hooks import NameFormatter import logging -logging.getLogger(__name__) +logger = logging.getLogger(__name__) class MumbleManager(models.Manager): @@ -48,7 +48,6 @@ class MumbleManager(models.Manager): user=user, username=username_clean, pwhash=pwhash, - hashfn=self.HashFunction.SHA256, display_name=display_name) result.update_groups() result.credentials.update({'username': result.username, 'password': password}) @@ -57,5 +56,5 @@ class MumbleManager(models.Manager): return False return False - def user_exists(self, username): + def user_exists(self, username) -> bool: return self.filter(username=username).exists() diff --git a/allianceauth/services/modules/mumble/migrations/0016_templink_alter_mumbleuser_last_connect_and_more.py b/allianceauth/services/modules/mumble/migrations/0016_templink_alter_mumbleuser_last_connect_and_more.py new file mode 100644 index 00000000..c9f3f0b7 --- /dev/null +++ b/allianceauth/services/modules/mumble/migrations/0016_templink_alter_mumbleuser_last_connect_and_more.py @@ -0,0 +1,60 @@ +# Generated by Django 4.2.16 on 2024-12-06 10:47 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('eveonline', '0017_alliance_and_corp_names_are_not_unique'), + ('mumble', '0015_alter_idlerhandler_options_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='TempLink', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('expires', models.DateTimeField(verbose_name='Expiry')), + ('link_ref', models.CharField(max_length=20)), + ('creator', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='eveonline.evecharacter')), + ], + options={ + 'permissions': (('create_new_links', 'Can Create Temp Links'),), + }, + ), + migrations.AlterField( + model_name='mumbleuser', + name='last_connect', + field=models.DateTimeField(blank=True, editable=False, help_text='Timestamp of the users Last Connection to Mumble', null=True, verbose_name='Last Connection'), + ), + migrations.AlterField( + model_name='mumbleuser', + name='last_disconnect', + field=models.DateTimeField(blank=True, editable=False, help_text='Timestamp of the users Last Disconnection from Mumble', null=True, verbose_name='Last Disconnection'), + ), + migrations.AlterField( + model_name='mumbleuser', + name='release', + field=models.TextField(blank=True, editable=False, help_text='Client release. For official releases, this equals the version. For snapshots and git compiles, this will be something else.', null=True, verbose_name='Mumble Release'), + ), + migrations.CreateModel( + name='TempUser', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=200)), + ('username', models.CharField(max_length=254, unique=True)), + ('pwhash', models.CharField(max_length=90)), + ('hashfn', models.CharField(choices=[('bcrypt-sha256', 'SHA256'), ('sha1', 'SHA1')], default='bcrypt-sha256', max_length=15)), + ('certhash', models.CharField(blank=True, editable=False, help_text='Hash of Mumble client certificate as presented when user authenticates', max_length=254, null=True, verbose_name='Certificate Hash')), + ('release', models.TextField(blank=True, editable=False, help_text='Client release. For official releases, this equals the version. For snapshots and git compiles, this will be something else.', null=True, verbose_name='Mumble Release')), + ('version', models.IntegerField(blank=True, editable=False, help_text='Client version. Major version in upper 16 bits, followed by 8 bits of minor version and 8 bits of patchlevel. Version 1.2.3 = 0x010203.', null=True, verbose_name='Mumble Version')), + ('last_connect', models.DateTimeField(blank=True, editable=False, help_text='Timestamp of the users Last Connection to Mumble', null=True, verbose_name='Last Connection')), + ('last_disconnect', models.DateTimeField(blank=True, editable=False, help_text='Timestamp of the users Last Disconnection from Mumble', null=True, verbose_name='Last Disconnection')), + ('expires', models.DateTimeField(verbose_name='Expiry')), + ('character_id', models.IntegerField(blank=True, default=None, null=True)), + ('templink', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='mumble.templink')), + ], + ), + ] diff --git a/allianceauth/services/modules/mumble/models.py b/allianceauth/services/modules/mumble/models.py index eeddf7f2..8e04f5b2 100644 --- a/allianceauth/services/modules/mumble/models.py +++ b/allianceauth/services/modules/mumble/models.py @@ -1,4 +1,5 @@ from typing import LiteralString +from allianceauth.eveonline.models import EveCharacter from allianceauth.services.hooks import NameFormatter from allianceauth.services.modules.mumble.managers import MumbleManager from django.db import models @@ -28,43 +29,24 @@ class MumbleUser(AbstractServiceModel): default=HashFunction.SHA256) certhash = models.CharField( verbose_name="Certificate Hash", - max_length=254, - blank=True, - null=True, - editable=False, - help_text="Hash of Mumble client certificate as presented when user authenticates" - ) + max_length=254,blank=True, null=True, editable=False, + help_text="Hash of Mumble client certificate as presented when user authenticates") release = models.TextField( verbose_name="Mumble Release", - max_length=254, - blank=True, - null=True, - editable=False, - help_text="Client release. For official releases, this equals the version. For snapshots and git compiles, this will be something else." - ) + blank=True, null=True, editable=False, + help_text="Client release. For official releases, this equals the version. For snapshots and git compiles, this will be something else.") version = models.IntegerField( verbose_name="Mumble Version", - blank=True, - null=True, - editable=False, - help_text="Client version. Major version in upper 16 bits, followed by 8 bits of minor version and 8 bits of patchlevel. Version 1.2.3 = 0x010203." - ) + blank=True, null=True, editable=False, + help_text="Client version. Major version in upper 16 bits, followed by 8 bits of minor version and 8 bits of patchlevel. Version 1.2.3 = 0x010203.") last_connect = models.DateTimeField( verbose_name="Last Connection", - max_length=254, - blank=True, - null=True, - editable=False, - help_text="Timestamp of the users Last Connection to Mumble" - ) + blank=True, null=True, editable=False, + help_text="Timestamp of the users Last Connection to Mumble") last_disconnect = models.DateTimeField( verbose_name="Last Disconnection", - max_length=254, - blank=True, - null=True, - editable=False, - help_text="Timestamp of the users Last Disconnection from Mumble" - ) + blank=True, null=True, editable=False, + help_text="Timestamp of the users Last Disconnection from Mumble") objects = MumbleManager() @@ -112,6 +94,55 @@ class MumbleUser(AbstractServiceModel): ) +class TempLink(models.Model): + expires = models.DateTimeField(_("Expiry"), auto_now=False, auto_now_add=False) + link_ref = models.CharField(max_length=20) + creator = models.ForeignKey(EveCharacter, on_delete=models.SET_NULL, null=True, default=None) + + class Meta: + permissions = (("create_new_links", "Can Create Temp Links"),) + + def __str__(self): + return f"Link {self.link_ref} - {self.expires}" + + +class TempUser(models.Model): + name = models.CharField(max_length=200) # Display name to show + username = models.CharField(max_length=254, unique=True) + pwhash = models.CharField(max_length=90) + hashfn = models.CharField( + max_length=15, + choices=MumbleUser.HashFunction.choices, + default=MumbleUser.HashFunction.SHA256) + certhash = models.CharField( + verbose_name="Certificate Hash", + max_length=254,blank=True, null=True, editable=False, + help_text="Hash of Mumble client certificate as presented when user authenticates") + release = models.TextField( + verbose_name="Mumble Release", + blank=True, null=True, editable=False, + help_text="Client release. For official releases, this equals the version. For snapshots and git compiles, this will be something else.") + version = models.IntegerField( + verbose_name="Mumble Version", + blank=True, null=True, editable=False, + help_text="Client version. Major version in upper 16 bits, followed by 8 bits of minor version and 8 bits of patchlevel. Version 1.2.3 = 0x010203.") + last_connect = models.DateTimeField( + verbose_name="Last Connection", + blank=True, null=True, editable=False, + help_text="Timestamp of the users Last Connection to Mumble") + last_disconnect = models.DateTimeField( + verbose_name="Last Disconnection", + blank=True, null=True, editable=False, + help_text="Timestamp of the users Last Disconnection from Mumble") + expires = models.DateTimeField(_("Expiry"), auto_now=False, auto_now_add=False) + + templink = models.ForeignKey(TempLink, on_delete=models.CASCADE, null=True, default=None) + character_id = models.IntegerField(default=None, blank=True, null=True) + + def __str__(self) -> str: + return f"Temp User: {self.username} - {self.name}" + + class IdlerHandler(models.Model): name = models.CharField(_("Name"), max_length=50) enabled = models.BooleanField(_("Enabled"), default=False) diff --git a/allianceauth/services/modules/mumble/tasks.py b/allianceauth/services/modules/mumble/tasks.py index 5ce7c9c4..e02a1c86 100644 --- a/allianceauth/services/modules/mumble/tasks.py +++ b/allianceauth/services/modules/mumble/tasks.py @@ -1,10 +1,11 @@ +from datetime import datetime, timezone import logging from django.contrib.auth.models import User from django.core.exceptions import ObjectDoesNotExist from celery import shared_task from allianceauth.services.tasks import QueueOnce -from .models import MumbleUser +from .models import MumbleUser, TempLink, TempUser logger = logging.getLogger(__name__) @@ -78,3 +79,10 @@ class MumbleTasks: logger.debug("Updating ALL mumble display names") for mumble_user in MumbleUser.objects.exclude(username__exact=''): MumbleTasks.update_display_name.delay(mumble_user.user.pk) + + +@shared_task +def tidy_up_temp_links(): + TempLink.objects.filter(expires__lt=datetime.now(timezone.utc).replace(tzinfo=timezone.utc).timestamp()).delete() + TempUser.objects.filter(templink__isnull=True).delete() + TempUser.objects.filter(expires__lt=datetime.now(timezone.utc).replace(tzinfo=timezone.utc).timestamp()).delete() diff --git a/allianceauth/services/modules/mumble/templates/services/mumble/base.html b/allianceauth/services/modules/mumble/templates/services/mumble/base.html new file mode 100644 index 00000000..c08c78a7 --- /dev/null +++ b/allianceauth/services/modules/mumble/templates/services/mumble/base.html @@ -0,0 +1,19 @@ +{% extends "allianceauth/base-bs5.html" %} + +{% load i18n %} + +{% block page_title %} + {% translate "Mumble Temporary Links" %} +{% endblock page_title %} + +{% block header_nav_brand %} + {% translate "Mumble Temporary Links" %} +{% endblock header_nav_brand %} + +{% block content %} +
+ {% translate 'Temp Links Give Access to mumble with the Guest Group for the preset time.' %} +
+ ++ {% translate 'Connected users will not be kicked at the end of this period.' %} +
+ ++ {% translate 'Templink users can be kicked in mumble by typing !kicktemps in mumble chat.' %} +
+ ++ {% translate 'There are no restrictions on who or how many can use a templink, share wisely.' %} +
+{% translate "Your link will be displayed on the next page for an easy copy and paste." %}
+ + ++ {% translate "Expires in" %}: +
+ +{{ SITE_URL }}{% url 'mumbletemps:join' tl.link_ref %}+ + +
+ | {% translate "Creator" %} | +{% translate "Key" %} | +{% translate "Time Left" %} | +{% translate "Copy" %} | +{% translate "Nuke" %} | +
---|---|---|---|---|---|
+ |
+
+ {{ lnk.creator.character_name }} | + +{{ lnk.link_ref }} | + ++ + | + + | + ++ {% translate "Nuke Link!" %} + | +
+ | {% translate "Creator" %} | +{% translate "Key" %} | +{% translate "Nuke" %} | +
---|---|---|---|
+ |
+
+ {{ lnk.creator.character_name }} | + +{{ lnk.link_ref }} | + ++ {% translate "Nuke Link!" %} + | +
+
+
+ {% translate "Time Remaining" %}: +
++ {% translate "Link Ref" %}: {{ link.link_ref }} +
+ +
+
+
+
+
+ {% translate "Click to Join Mumble as" %}: {{ temp_user.name }}
+
+
+
+ {% translate "If you have problems with the application link above, please use the following in Mumble's connection dialog." %} +
++ {% translate "Mumble URL" %}: {{ mumble }} +
++ {% translate "Username" %}: {{ temp_user.username }} +
++ {% translate "Password" %}: {{ temp_user.password }} +
+
+
+
+ {% translate "Time Remaining" %}: +
++ {% translate "Link Ref" %}: {{ link.link_ref }} +
+ + +