From cc1f94cf61b5ecc743c4911780a33a0b3c7f1e0a Mon Sep 17 00:00:00 2001 From: Aaron Kable Date: Mon, 6 Apr 2020 02:19:53 +0000 Subject: [PATCH] Mumble Display Names --- .../services/modules/discord/auth_hooks.py | 2 +- .../services/modules/mumble/auth_hooks.py | 5 ++ .../0008_mumbleuser_display_name.py | 17 +++++++ .../0009_set_mumble_dissplay_names.py | 37 ++++++++++++++ .../services/modules/mumble/models.py | 48 +++++++++++++------ allianceauth/services/modules/mumble/tasks.py | 28 +++++++++++ allianceauth/services/modules/mumble/tests.py | 30 ++++++++++-- allianceauth/services/signals.py | 39 +++++++++++++-- tox.ini | 1 + 9 files changed, 185 insertions(+), 22 deletions(-) create mode 100644 allianceauth/services/modules/mumble/migrations/0008_mumbleuser_display_name.py create mode 100644 allianceauth/services/modules/mumble/migrations/0009_set_mumble_dissplay_names.py diff --git a/allianceauth/services/modules/discord/auth_hooks.py b/allianceauth/services/modules/discord/auth_hooks.py index 57caa83e..6cd14973 100644 --- a/allianceauth/services/modules/discord/auth_hooks.py +++ b/allianceauth/services/modules/discord/auth_hooks.py @@ -36,7 +36,7 @@ class DiscordService(ServicesHook): def sync_nickname(self, user): logger.debug('Syncing %s nickname for user %s' % (self.name, user)) - DiscordTasks.update_nickname.delay(user.pk) + DiscordTasks.update_nickname.apply_async(args=[user.pk], countdown=5) def update_all_groups(self): logger.debug('Update all %s groups called' % self.name) diff --git a/allianceauth/services/modules/mumble/auth_hooks.py b/allianceauth/services/modules/mumble/auth_hooks.py index e5bafa5e..95bcf10f 100644 --- a/allianceauth/services/modules/mumble/auth_hooks.py +++ b/allianceauth/services/modules/mumble/auth_hooks.py @@ -40,6 +40,11 @@ class MumbleService(ServicesHook): if MumbleTasks.has_account(user): MumbleTasks.update_groups.delay(user.pk) + def sync_nickname(self, user): + logger.debug("Updating %s nickname for %s" % (self.name, user)) + if MumbleTasks.has_account(user): + MumbleTasks.update_display_name.apply_async(args=[user.pk], countdown=5) # cooldown on this task to ensure DB clean when syncing + def validate_user(self, user): if MumbleTasks.has_account(user) and not self.service_active_for_user(user): self.delete_user(user, notify_user=True) diff --git a/allianceauth/services/modules/mumble/migrations/0008_mumbleuser_display_name.py b/allianceauth/services/modules/mumble/migrations/0008_mumbleuser_display_name.py new file mode 100644 index 00000000..4a4a679d --- /dev/null +++ b/allianceauth/services/modules/mumble/migrations/0008_mumbleuser_display_name.py @@ -0,0 +1,17 @@ +# Generated by Django 2.2.9 on 2020-03-16 07:49 + +from django.db import migrations, models + +class Migration(migrations.Migration): + + dependencies = [ + ('mumble', '0007_not_null_user'), + ] + + operations = [ + migrations.AddField( + model_name='mumbleuser', + name='display_name', + field=models.CharField(max_length=254, null=True), + ) + ] diff --git a/allianceauth/services/modules/mumble/migrations/0009_set_mumble_dissplay_names.py b/allianceauth/services/modules/mumble/migrations/0009_set_mumble_dissplay_names.py new file mode 100644 index 00000000..c81f54dc --- /dev/null +++ b/allianceauth/services/modules/mumble/migrations/0009_set_mumble_dissplay_names.py @@ -0,0 +1,37 @@ +from django.db import migrations, models +from ..auth_hooks import MumbleService +from allianceauth.services.hooks import NameFormatter + +def fwd_func(apps, schema_editor): + MumbleUser = apps.get_model("mumble", "MumbleUser") + db_alias = schema_editor.connection.alias + all_users = MumbleUser.objects.using(db_alias).all() + for user in all_users: + display_name = NameFormatter(MumbleService(), user.user).format_name() + user.display_name = display_name + user.save() + +def rev_func(apps, schema_editor): + MumbleUser = apps.get_model("mumble", "MumbleUser") + db_alias = schema_editor.connection.alias + all_users = MumbleUser.objects.using(db_alias).all() + for user in all_users: + user.display_name = None + user.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('mumble', '0008_mumbleuser_display_name'), + ] + + operations = [ + migrations.RunPython(fwd_func, rev_func), + migrations.AlterField( + model_name='mumbleuser', + name='display_name', + field=models.CharField(max_length=254, unique=True), + preserve_default=False, + ), + ] diff --git a/allianceauth/services/modules/mumble/models.py b/allianceauth/services/modules/mumble/models.py index 221c63db..db9757dd 100644 --- a/allianceauth/services/modules/mumble/models.py +++ b/allianceauth/services/modules/mumble/models.py @@ -15,10 +15,14 @@ class MumbleManager(models.Manager): HASH_FN = 'bcrypt-sha256' @staticmethod - def get_username(user): + def get_display_name(user): from .auth_hooks import MumbleService return NameFormatter(MumbleService(), user).format_name() - + + @staticmethod + def get_username(user): + return user.profile.main_character.character_name # main character as the user.username may be incorect + @staticmethod def sanitise_username(username): return username.replace(" ", "_") @@ -32,20 +36,26 @@ class MumbleManager(models.Manager): return bcrypt_sha256.encrypt(password.encode('utf-8')) def create(self, user): - username = self.get_username(user) - logger.debug("Creating mumble user with username {}".format(username)) - username_clean = self.sanitise_username(username) - password = self.generate_random_pass() - pwhash = self.gen_pwhash(password) - logger.debug("Proceeding with mumble user creation: clean username {}, pwhash starts with {}".format( - username_clean, pwhash[0:5])) - logger.info("Creating mumble user {}".format(username_clean)) + try: + username = self.get_username(user) + logger.debug("Creating mumble user with username {}".format(username)) + username_clean = self.sanitise_username(username) + display_name = self.get_display_name(user) + password = self.generate_random_pass() + pwhash = self.gen_pwhash(password) + logger.debug("Proceeding with mumble user creation: clean username {}, pwhash starts with {}".format( + username_clean, pwhash[0:5])) + logger.info("Creating mumble user {}".format(username_clean)) - result = super(MumbleManager, self).create(user=user, username=username_clean, - pwhash=pwhash, hashfn=self.HASH_FN) - result.update_groups() - result.credentials.update({'username': result.username, 'password': password}) - return result + result = super(MumbleManager, self).create(user=user, username=username_clean, + pwhash=pwhash, hashfn=self.HASH_FN, + display_name=display_name) + result.update_groups() + result.credentials.update({'username': result.username, 'password': password}) + return result + except AttributeError: # No Main or similar errors + return False + return False def user_exists(self, username): return self.filter(username=username).exists() @@ -59,6 +69,8 @@ class MumbleUser(AbstractServiceModel): objects = MumbleManager() + display_name = models.CharField(max_length=254, unique=True) + def __str__(self): return self.username @@ -91,6 +103,12 @@ class MumbleUser(AbstractServiceModel): self.save() return True + def update_display_name(self): + logger.info("Updating mumble user {} display name".format(self.user)) + self.display_name = MumbleManager.get_display_name(self.user) + self.save() + return True + class Meta: permissions = ( ("access_mumble", u"Can access the Mumble service"), diff --git a/allianceauth/services/modules/mumble/tasks.py b/allianceauth/services/modules/mumble/tasks.py index 02450e93..e318fb06 100644 --- a/allianceauth/services/modules/mumble/tasks.py +++ b/allianceauth/services/modules/mumble/tasks.py @@ -45,9 +45,37 @@ class MumbleTasks: logger.debug("User %s does not have a mumble account, skipping" % user) return False + @staticmethod + @shared_task(bind=True, name="mumble.update_display_name", base=QueueOnce) + def update_display_name(self, pk): + user = User.objects.get(pk=pk) + logger.debug("Updating mumble groups for user %s" % user) + if MumbleTasks.has_account(user): + try: + if not user.mumble.update_display_name(): + raise Exception("Display Name Sync failed") + logger.debug("Updated user %s mumble display name." % user) + return True + except MumbleUser.DoesNotExist: + logger.info("Mumble display name sync failed for {}, user does not have a mumble account".format(user)) + except: + logger.exception("Mumble display name sync failed for %s, retrying in 10 mins" % user) + raise self.retry(countdown=60 * 10) + else: + logger.debug("User %s does not have a mumble account, skipping" % user) + return False + @staticmethod @shared_task(name="mumble.update_all_groups") def update_all_groups(): logger.debug("Updating ALL mumble groups") for mumble_user in MumbleUser.objects.exclude(username__exact=''): MumbleTasks.update_groups.delay(mumble_user.user.pk) + + @staticmethod + @shared_task(name="mumble.update_all_display_names") + def update_all_display_names(): + 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) + diff --git a/allianceauth/services/modules/mumble/tests.py b/allianceauth/services/modules/mumble/tests.py index c7940058..06c961c1 100644 --- a/allianceauth/services/modules/mumble/tests.py +++ b/allianceauth/services/modules/mumble/tests.py @@ -25,6 +25,9 @@ class MumbleHooksTestCase(TestCase): def setUp(self): self.member = 'member_user' member = AuthUtils.create_member(self.member) + AuthUtils.add_main_character(member, 'auth_member', '12345', corp_id='111', corp_name='Test Corporation', + corp_ticker='TESTR') + member = User.objects.get(pk=member.pk) MumbleUser.objects.create(user=member) self.none_user = 'none_user' none_user = AuthUtils.create_user(self.none_user) @@ -122,23 +125,45 @@ class MumbleViewsTestCase(TestCase): self.member.save() AuthUtils.add_main_character(self.member, 'auth_member', '12345', corp_id='111', corp_name='Test Corporation', corp_ticker='TESTR') + self.member = User.objects.get(pk=self.member.pk) add_permissions() def login(self): self.client.force_login(self.member) - def test_activate(self): + def test_activate_update(self): self.login() - expected_username = '[TESTR]auth_member' + expected_username = 'auth_member' + expected_displayname = '[TESTR]auth_member' response = self.client.get(urls.reverse('mumble:activate'), follow=False) self.assertEqual(response.status_code, 200) self.assertContains(response, expected_username) + # create mumble_user = MumbleUser.objects.get(user=self.member) self.assertEqual(mumble_user.username, expected_username) + self.assertTrue(MumbleUser.objects.user_exists(expected_username)) + self.assertEqual(str(mumble_user), expected_username) + self.assertEqual(mumble_user.display_name, expected_displayname) self.assertTrue(mumble_user.pwhash) self.assertIn('Guest', mumble_user.groups) self.assertIn('Member', mumble_user.groups) self.assertIn(',', mumble_user.groups) + # test update + self.member.profile.main_character.character_name = "auth_member_updated" + self.member.profile.main_character.corporation_ticker = "TESTU" + self.member.profile.main_character.save() + mumble_user.update_display_name() + mumble_user = MumbleUser.objects.get(user=self.member) + expected_displayname = '[TESTU]auth_member_updated' + self.assertEqual(mumble_user.username, expected_username) + self.assertTrue(MumbleUser.objects.user_exists(expected_username)) + self.assertEqual(str(mumble_user), expected_username) + self.assertEqual(mumble_user.display_name, expected_displayname) + self.assertTrue(mumble_user.pwhash) + self.assertIn('Guest', mumble_user.groups) + self.assertIn('Member', mumble_user.groups) + self.assertIn(',', mumble_user.groups) + def test_deactivate_post(self): self.login() @@ -171,7 +196,6 @@ class MumbleViewsTestCase(TestCase): self.assertTemplateUsed(response, 'services/service_credentials.html') self.assertContains(response, 'auth_member') - class MumbleManagerTestCase(TestCase): def setUp(self): from .models import MumbleManager diff --git a/allianceauth/services/signals.py b/allianceauth/services/signals.py index 912a5a4c..0476954d 100644 --- a/allianceauth/services/signals.py +++ b/allianceauth/services/signals.py @@ -1,6 +1,7 @@ import logging from django.contrib.auth.models import User, Group, Permission +from django.core.exceptions import ObjectDoesNotExist from django.db import transaction from django.db.models.signals import m2m_changed from django.db.models.signals import pre_delete @@ -11,6 +12,7 @@ from .tasks import disable_user from allianceauth.authentication.models import State, UserProfile from allianceauth.authentication.signals import state_changed +from allianceauth.eveonline.models import EveCharacter logger = logging.getLogger(__name__) @@ -157,14 +159,45 @@ def disable_services_on_inactive(sender, instance, *args, **kwargs): @receiver(pre_save, sender=UserProfile) -def disable_services_on_no_main(sender, instance, *args, **kwargs): - if not instance.pk: +def process_main_character_change(sender, instance, *args, **kwargs): + + if not instance.pk: # ignore # new model being created return try: old_instance = UserProfile.objects.get(pk=instance.pk) - if old_instance.main_character and not instance.main_character: + if old_instance.main_character and not instance.main_character: # lost main char disable services logger.info("Disabling services due to loss of main character for user {0}".format(instance.user)) disable_user(instance.user) + elif old_instance.main_character is not instance.main_character: # swapping/changing main character + logger.info("Updating Names due to change of main character for user {0}".format(instance.user)) + for svc in ServicesHook.get_services(): + try: + svc.validate_user(instance.user) + svc.sync_nickname(instance.user) + except: + logger.exception('Exception running sync_nickname for services module %s on user %s' % (svc, instance)) + except UserProfile.DoesNotExist: pass + + +@receiver(pre_save, sender=EveCharacter) +def process_main_character_update(sender, instance, *args, **kwargs): + try: + if instance.userprofile: + old_instance = EveCharacter.objects.get(pk=instance.pk) + if not instance.character_name == old_instance.character_name or \ + not instance.corporation_name == old_instance.corporation_name or \ + not instance.alliance_name == old_instance.alliance_name: + logger.info("syncing service nickname for user {0}".format(instance.userprofile.user)) + + for svc in ServicesHook.get_services(): + try: + svc.validate_user(instance.userprofile.user) + svc.sync_nickname(instance.userprofile.user) + except: + logger.exception('Exception running sync_nickname for services module %s on user %s' % (svc, instance)) + + except ObjectDoesNotExist: # not a main char ignore + pass diff --git a/tox.ini b/tox.ini index 53c0fc18..c881a1fb 100644 --- a/tox.ini +++ b/tox.ini @@ -19,4 +19,5 @@ install_command = pip install -e ".[testing]" -U {opts} {packages} commands = all: coverage run runtests.py -v 2 all: coverage report -m + all: coverage xml core: coverage run runtests.py allianceauth.authentication.tests.test_app_settings -v 2