diff --git a/baseapp-chats/baseapp_chats/admin.py b/baseapp-chats/baseapp_chats/admin.py index 1d31373e..c155b605 100644 --- a/baseapp-chats/baseapp_chats/admin.py +++ b/baseapp-chats/baseapp_chats/admin.py @@ -34,7 +34,7 @@ class MessageAdmin(admin.ModelAdmin): @admin.register(ChatRoom) class ChatRoomAdmin(admin.ModelAdmin): - list_display = ("room",) + list_display = ("room", "title", "is_group") search_fields = ["room"] inlines = [ChatRoomParticipantInline] diff --git a/baseapp-chats/baseapp_chats/base.py b/baseapp-chats/baseapp_chats/base.py index 70ff8d28..940ee597 100644 --- a/baseapp-chats/baseapp_chats/base.py +++ b/baseapp-chats/baseapp_chats/base.py @@ -115,6 +115,7 @@ def description(self): ) action_object_object_id = models.IntegerField(blank=True, null=True, db_index=True) action_object = GenericForeignKey("action_object_content_type", "action_object_object_id") + deleted = models.BooleanField(default=False) extra_data = models.JSONField(blank=True, null=True) diff --git a/baseapp-chats/baseapp_chats/graphql/mutations.py b/baseapp-chats/baseapp_chats/graphql/mutations.py index 64363bb1..b837e399 100644 --- a/baseapp-chats/baseapp_chats/graphql/mutations.py +++ b/baseapp-chats/baseapp_chats/graphql/mutations.py @@ -15,8 +15,8 @@ from rest_framework import serializers from baseapp_chats.graphql.subscriptions import ( + ChatRoomOnMessage, ChatRoomOnMessagesCountUpdate, - ChatRoomOnNewMessage, ChatRoomOnRoomUpdate, ) from baseapp_chats.utils import ( @@ -496,7 +496,7 @@ def mutate_and_get_payload(cls, root, info, **input): message.content = content message.save(update_fields=["content"]) - ChatRoomOnNewMessage.new_message(room_id=message.room.relay_id, message=message) + ChatRoomOnMessage.edit_message(room_id=message.room.relay_id, message=message) return ChatRoomEditMessage( message=MessageObjectType._meta.connection.Edge( @@ -505,6 +505,72 @@ def mutate_and_get_payload(cls, root, info, **input): ) +class ChatRoomDeleteMessage(RelayMutation): + deleted_message = graphene.Field(MessageObjectType._meta.connection.Edge) + + class Input: + id = graphene.ID(required=True) + + @classmethod + @login_required + def mutate_and_get_payload(cls, root, info, **input): + pk = get_pk_from_relay_id(input.get("id")) + try: + message = Message.objects.get(pk=pk) + except Message.DoesNotExist: + return ChatRoomDeleteMessage( + errors=[ + ErrorType( + field="id", + messages=[_("Message does not exist")], + ) + ] + ) + + if message.deleted: + return ChatRoomDeleteMessage( + errors=[ + ErrorType( + field="deleted", + messages=[_("This message has already been deleted")], + ) + ] + ) + + profile = ( + info.context.user.current_profile + if hasattr(info.context.user, "current_profile") + else (info.context.user.profile if hasattr(info.context.user, "profile") else None) + ) + + if not info.context.user.has_perm( + "baseapp_chats.delete_message", + { + "profile": profile, + "message": message, + }, + ): + return ChatRoomDeleteMessage( + errors=[ + ErrorType( + field="id", + messages=[_("You don't have permission to update this message")], + ) + ] + ) + + message.deleted = True + message.save(update_fields=["deleted"]) + + ChatRoomOnMessage.edit_message(room_id=message.room.relay_id, message=message) + + return ChatRoomDeleteMessage( + deleted_message=MessageObjectType._meta.connection.Edge( + node=message, + ) + ) + + class ChatRoomReadMessages(RelayMutation): room = graphene.Field(ChatRoomObjectType) profile = graphene.Field(ProfileObjectType) @@ -683,6 +749,7 @@ class ChatsMutations(object): chat_room_update = ChatRoomUpdate.Field() chat_room_send_message = ChatRoomSendMessage.Field() chat_room_edit_message = ChatRoomEditMessage.Field() + chat_room_delete_message = ChatRoomDeleteMessage.Field() chat_room_read_messages = ChatRoomReadMessages.Field() chat_room_unread = ChatRoomUnread.Field() chat_room_archive = ChatRoomArchive.Field() diff --git a/baseapp-chats/baseapp_chats/graphql/object_types.py b/baseapp-chats/baseapp_chats/graphql/object_types.py index 68090b08..09451e6a 100644 --- a/baseapp-chats/baseapp_chats/graphql/object_types.py +++ b/baseapp-chats/baseapp_chats/graphql/object_types.py @@ -65,6 +65,7 @@ class Meta: "extra_data", "in_reply_to", "is_read", + "deleted", ) filter_fields = ("verb",) @@ -109,6 +110,10 @@ def resolve_content(root, info, profile_id=None, **kwargs): profile_pk = BaseMessageObjectType.get_profile_pk(info, profile_id) if not profile_pk: return None + if root.deleted: + if profile_pk == root.profile.pk: + return "You deleted this message" + return "This message was deleted" if root.message_type == Message.MessageType.USER_MESSAGE: return root.content diff --git a/baseapp-chats/baseapp_chats/graphql/subscriptions.py b/baseapp-chats/baseapp_chats/graphql/subscriptions.py index eea205b0..aab7b6fc 100644 --- a/baseapp-chats/baseapp_chats/graphql/subscriptions.py +++ b/baseapp-chats/baseapp_chats/graphql/subscriptions.py @@ -85,7 +85,7 @@ def send_updated_chat_count(cls, profile, profile_id): ) -class ChatRoomOnNewMessage(channels_graphql_ws.Subscription): +class ChatRoomOnMessage(channels_graphql_ws.Subscription): message = graphene.Field(lambda: MessageObjectType._meta.connection.Edge) class Arguments: @@ -110,7 +110,7 @@ def publish(payload, info, room_id): if not user.is_authenticated: return None - return ChatRoomOnNewMessage(message=MessageObjectType._meta.connection.Edge(node=message)) + return ChatRoomOnMessage(message=MessageObjectType._meta.connection.Edge(node=message)) @classmethod def new_message(cls, message, room_id): @@ -120,8 +120,15 @@ def new_message(cls, message, room_id): ) ChatRoomOnRoomUpdate.new_message(message=message) + @classmethod + def edit_message(cls, message, room_id): + cls.broadcast( + group=room_id, + payload={"message": message}, + ) + class ChatsSubscriptions: - chat_room_on_new_message = ChatRoomOnNewMessage.Field() + chat_room_on_message = ChatRoomOnMessage.Field() chat_room_on_room_update = ChatRoomOnRoomUpdate.Field() chat_room_on_messages_count_update = ChatRoomOnMessagesCountUpdate.Field() diff --git a/baseapp-chats/baseapp_chats/migrations/0010_message_deleted.py b/baseapp-chats/baseapp_chats/migrations/0010_message_deleted.py new file mode 100644 index 00000000..8fc4d615 --- /dev/null +++ b/baseapp-chats/baseapp_chats/migrations/0010_message_deleted.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.10 on 2025-02-04 17:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("baseapp_chats", "0009_remove_message_create_message_status_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="message", + name="deleted", + field=models.BooleanField(default=False), + ), + ] diff --git a/baseapp-chats/baseapp_chats/permissions.py b/baseapp-chats/baseapp_chats/permissions.py index 0de8823d..6c122e1a 100644 --- a/baseapp-chats/baseapp_chats/permissions.py +++ b/baseapp-chats/baseapp_chats/permissions.py @@ -127,7 +127,9 @@ def has_perm(self, user_obj, perm, obj=None): return room.participants.filter(profile_id__in=my_profile_ids).exists() - if perm == "baseapp_chats.change_message" and user_obj.is_authenticated: + if ( + perm == "baseapp_chats.change_message" or perm == "baseapp_chats.delete_message" + ) and user_obj.is_authenticated: profile = obj.get("profile", None) message = obj.get("message", None) if profile and message and isinstance(message, Message): diff --git a/baseapp-chats/baseapp_chats/tests/test_graphql_mutations.py b/baseapp-chats/baseapp_chats/tests/test_graphql_mutations.py index 0ee14b02..929c9599 100644 --- a/baseapp-chats/baseapp_chats/tests/test_graphql_mutations.py +++ b/baseapp-chats/baseapp_chats/tests/test_graphql_mutations.py @@ -181,6 +181,23 @@ } """ +DELETE_MESSAGE_GRAPHQL = """ + mutation DeleteMessageMutation($input: ChatRoomDeleteMessageInput!) { + chatRoomDeleteMessage(input: $input) { + deletedMessage { + node { + id + deleted + } + } + errors { + field + messages + } + } + } +""" + def test_user_can_read_all_messages(graphql_user_client, django_user_client): room = ChatRoomFactory(created_by=django_user_client.user) @@ -1281,3 +1298,62 @@ def test_member_user_cannot_remove_other_members( content["data"]["chatRoomUpdate"]["errors"][0]["messages"][0] == "You don't have permission to update this room" ) + + +def test_user_can_delete_own_message(graphql_user_client, django_user_client): + room = ChatRoomFactory(created_by=django_user_client.user) + + my_profile = django_user_client.user.profile + + ChatRoomParticipantFactory(room=room, profile=my_profile) + my_messages = MessageFactory.create_batch( + 2, room=room, profile=my_profile, user=my_profile.owner + ) + + # Unread chat and check it is marked unread + response = graphql_user_client( + DELETE_MESSAGE_GRAPHQL, + variables={ + "input": { + "id": my_messages[0].relay_id, + }, + }, + ) + + my_messages[0].refresh_from_db() + my_messages[1].refresh_from_db() + content = response.json() + assert ( + content["data"]["chatRoomDeleteMessage"]["deletedMessage"]["node"]["id"] + == my_messages[0].relay_id + ) + assert content["data"]["chatRoomDeleteMessage"]["deletedMessage"]["node"]["deleted"] is True + assert my_messages[0].deleted is True + assert my_messages[1].deleted is False + + +def test_user_cant_delete_other_users_message(graphql_user_client, django_user_client): + room = ChatRoomFactory(created_by=django_user_client.user) + + my_profile = django_user_client.user.profile + + ChatRoomParticipantFactory(room=room, profile=my_profile) + other_participant = ChatRoomParticipantFactory(room=room) + other_users_messages = MessageFactory.create_batch( + 2, room=room, profile=other_participant.profile, user=other_participant.profile.owner + ) + + # Unread chat and check it is marked unread + response = graphql_user_client( + DELETE_MESSAGE_GRAPHQL, + variables={ + "input": { + "id": other_users_messages[0].relay_id, + }, + }, + ) + content = response.json() + assert ( + content["data"]["chatRoomDelete"]["errors"][0]["messages"][0] + == "You don't have permission to update this room" + ) diff --git a/baseapp-chats/baseapp_chats/utils.py b/baseapp-chats/baseapp_chats/utils.py index 853b1ddd..39c84f73 100644 --- a/baseapp-chats/baseapp_chats/utils.py +++ b/baseapp-chats/baseapp_chats/utils.py @@ -43,14 +43,13 @@ def send_message( action_object=action_object, extra_data=extra_data, ) - - from baseapp_chats.graphql.subscriptions import ChatRoomOnNewMessage + from baseapp_chats.graphql.subscriptions import ChatRoomOnMessage room.last_message_time = message.created room.last_message = message room.save() - ChatRoomOnNewMessage.new_message(room_id=room_id or room.relay_id, message=message) + ChatRoomOnMessage.new_message(room_id=room_id or room.relay_id, message=message) return message diff --git a/baseapp-chats/setup.cfg b/baseapp-chats/setup.cfg index ebbe04bd..a8fc6847 100644 --- a/baseapp-chats/setup.cfg +++ b/baseapp-chats/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = baseapp_chats -version = 0.0.14 +version = 0.0.15 description = BaseApp Chats long_description = file: README.md url = https://github.com/silverlogic/baseapp-backend