import random import string from django.db.models.signals import post_save, pre_delete, post_delete, m2m_changed from django.db.transaction import DatabaseError from django.dispatch import receiver from django.conf import settings from django.apps import apps from django.utils import timezone from django.db.models import Q from .models import Club, FailedApiCall, CustomUser, Log, DataAccess, ModelLog, BaseModel import requests from channels.layers import get_channel_layer from asgiref.sync import async_to_sync # Synchronization @receiver([post_save, post_delete]) def synchronization_notifications(sender, instance, **kwargs): """ Signal handler that sends notifications through WebSocket channels when model instances are saved or deleted. The function creates a WebSocket group name for each affected user and sends a sync update message to all clients connected to that group. """ if sender in [FailedApiCall, Log, ModelLog]: return channel_layer = get_channel_layer() user_ids = set() if isinstance(instance, CustomUser): user_ids.add(instance.id) elif isinstance(instance, DataAccess): for shared_user in instance.shared_with.all(): user_ids.add(shared_user.id) elif isinstance(instance, BaseModel): owner = instance.get_owner() if owner is not None: user_ids.add(owner.id) if isinstance(instance, BaseModel): parent_model, data_access_reference_id = instance.get_parent_reference() data_access_query = Q(model_id=instance.id) if data_access_reference_id is not None: data_access_query |= Q(model_id=data_access_reference_id) data_access_list = DataAccess.objects.filter(data_access_query) for data_access in data_access_list: user_ids.add(data_access.owner.id) for shared_user in data_access.shared_with.all(): user_ids.add(shared_user.id) for user_id in user_ids: group_name = f"sync_{user_id}" print(f">>> send to group {group_name}") # Send to all clients in the sync group async_to_sync(channel_layer.group_send)( group_name, {"type": "sync.update", "message": "hello"} ) @receiver(m2m_changed, sender=DataAccess.shared_with.through) def handle_shared_with_changes(sender, instance, action, pk_set, **kwargs): if action == "post_add": for user_id in pk_set: user = CustomUser.objects.get(id=user_id) instance.create_access_log(user, 'GRANT_ACCESS') elif action == "post_remove": for user_id in pk_set: user = CustomUser.objects.get(id=user_id) instance.create_access_log(user, 'REVOKE_ACCESS') @receiver(pre_delete, sender=DataAccess) def store_users_before_delete(sender, instance, **kwargs): # Store the users in a temporary attribute that we can access after deletion instance._users_to_revoke = list(instance.shared_with.all()) @receiver(post_delete, sender=DataAccess) def revoke_access_after_delete(sender, instance, **kwargs): # Create revoke logs for all previously stored users if hasattr(instance, '_users_to_revoke'): for user in instance._users_to_revoke: instance.create_access_log(user, 'REVOKE_ACCESS') # Others def generate_unique_code(): characters = string.ascii_letters + string.digits while True: code = ''.join(random.sample(characters, 3)) if not Club.objects.filter(broadcast_code=code).exists(): return code @receiver(post_save, sender=Club) def assign_unique_code(sender, instance, created, **kwargs): if created and not instance.broadcast_code: instance.broadcast_code = generate_unique_code() instance.save() DISCORD_FAILED_CALLS_WEBHOOK_URL = 'https://discord.com/api/webhooks/1248191778134163486/sSoTL6cULCElWr2YFwyllsg7IXxHcCx_YMDJA_cUHtVUU4WOfN-5M7drCJuwNBBfAk9a' DISCORD_LOGS_WEBHOOK_URL = 'https://discord.com/api/webhooks/1257987637449588736/TtOUwzYgSlQH2d3Ps7SfIKRcFALQVa3hfkC-j9K4_UAcWtsfiw4v8NUPbnX2_ZPOYzuv' @receiver(post_save, sender=FailedApiCall) def notify_discord_on_create(sender, instance, created, **kwargs): notify_object_creation_on_discord(created, instance, DISCORD_FAILED_CALLS_WEBHOOK_URL) @receiver(post_save, sender=Log) def notify_log_creation_on_discord(sender, instance, created, **kwargs): notify_object_creation_on_discord(created, instance, DISCORD_LOGS_WEBHOOK_URL) # WARNING: using this method requires the instance to have a discord_string method def notify_object_creation_on_discord(created, instance, webhook_url): if created: default_db_engine = settings.DATABASES['default']['ENGINE'] if default_db_engine != 'django.db.backends.sqlite3': if hasattr(instance, 'discord_string'): message = f'New {instance.__class__.__name__} created: {instance.discord_string()}' else: message = "no message. Please configure 'discord_string' on your instance" send_discord_message(webhook_url, message) def send_discord_message(webhook_url, content): data = { "content": content } response = requests.post(webhook_url, json=data) if response.status_code != 204: raise ValueError( f'Error sending message to Discord webhook: {response.status_code}, {response.text}' )