Coverage for modules\chat\api.py: 36%

58 statements  

« prev     ^ index     » next       coverage.py v7.16.0, created at 2026-09-05 09:25 +0800

1from ninja import Router 

2from typing import List, Optional 

3from uuid import UUID 

4from core.auth import JWTAuth 

5from core.permissions import require_tenant 

6from core.exceptions import NotFoundException 

7from .schemas import ( 

8 ChannelOut, ChannelCreateIn, ChannelMemberOut, 

9 MessageOut, MessageSendIn, MessageAttachmentOut, MessageReactionOut 

10) 

11from .models import Channel, ChannelMember, Message, MessageReaction 

12from .services import ChatService 

13from modules.authentication.models import User 

14 

15router = Router(tags=["Real-Time Chat"]) 

16 

17@router.get("/channels", auth=JWTAuth(), response={200: List[ChannelOut]}) 

18def list_channels(request): 

19 tenant_id = require_tenant(request) 

20 ChatService.get_or_create_general_channel(tenant_id, request.user) 

21 channels = Channel.objects.filter( 

22 tenant_id=tenant_id, 

23 members__user=request.user 

24 ).distinct().prefetch_related('messages') 

25 

26 result = [] 

27 for ch in channels: 

28 last_msg = ch.messages.order_by('-created_at').select_related('sender').first() 

29 last_msg_preview = None 

30 if last_msg: 

31 last_msg_preview = { 

32 'id': last_msg.id, 

33 'sender_name': last_msg.sender.full_name, 

34 'content': last_msg.content[:50], 

35 'created_at': last_msg.created_at, 

36 } 

37 

38 result.append(ChannelOut( 

39 id=ch.id, 

40 name=ch.name, 

41 topic=ch.topic, 

42 type=ch.type, 

43 created_by_id=ch.created_by_id, 

44 members_count=ch.members.count(), 

45 unread_count=0, 

46 last_message=last_msg_preview, 

47 created_at=ch.created_at, 

48 )) 

49 return result 

50 

51@router.post("/channels", auth=JWTAuth(), response={201: ChannelOut}) 

52def create_channel(request, payload: ChannelCreateIn): 

53 tenant_id = require_tenant(request) 

54 ch = ChatService.create_channel(tenant_id, request.user, payload) 

55 return 201, ChannelOut( 

56 id=ch.id, 

57 name=ch.name, 

58 topic=ch.topic, 

59 type=ch.type, 

60 created_by_id=ch.created_by_id, 

61 members_count=ch.members.count(), 

62 unread_count=0, 

63 last_message=None, 

64 created_at=ch.created_at, 

65 ) 

66 

67@router.post("/channels/dm", auth=JWTAuth(), response={200: ChannelOut}) 

68def create_or_get_dm(request, target_user_id: UUID): 

69 tenant_id = require_tenant(request) 

70 target_user = User.objects.filter(id=target_user_id).first() 

71 if not target_user: 

72 raise NotFoundException("Target user not found.") 

73 

74 ch = ChatService.create_direct_message_channel(tenant_id, request.user, target_user) 

75 return ChannelOut( 

76 id=ch.id, 

77 name=f"Direct: {target_user.full_name}", 

78 topic=None, 

79 type='DIRECT', 

80 created_by_id=ch.created_by_id, 

81 members_count=2, 

82 unread_count=0, 

83 last_message=None, 

84 created_at=ch.created_at, 

85 ) 

86 

87@router.get("/channels/{channel_id}/messages", auth=JWTAuth(), response={200: List[MessageOut]}) 

88def list_messages(request, channel_id: UUID, limit: int = 50): 

89 tenant_id = require_tenant(request) 

90 channel = Channel.objects.filter(id=channel_id, tenant_id=tenant_id).first() 

91 if not channel: 

92 raise NotFoundException("Channel not found.") 

93 

94 messages = Message.objects.filter(channel=channel).select_related('sender').prefetch_related('attachments', 'reactions')[:limit] 

95 result = [] 

96 for m in messages: 

97 reactions_map = {} 

98 for r in m.reactions.all(): 

99 if r.emoji not in reactions_map: 

100 reactions_map[r.emoji] = [] 

101 reactions_map[r.emoji].append({'id': str(r.user_id), 'name': r.user.full_name}) 

102 

103 result.append(MessageOut( 

104 id=m.id, 

105 channel_id=m.channel_id, 

106 sender_id=m.sender.id, 

107 sender_name=m.sender.full_name, 

108 sender_avatar=m.sender.avatar_url, 

109 content=m.content, 

110 parent_id=m.parent_id, 

111 attachments=[ 

112 MessageAttachmentOut( 

113 id=a.id, 

114 file_name=a.file_name, 

115 file_size=a.file_size, 

116 file_type=a.file_type, 

117 file_url=a.file_url, 

118 ) for a in m.attachments.all() 

119 ], 

120 reactions=[ 

121 MessageReactionOut( 

122 emoji=emoji, 

123 count=len(users), 

124 users=users 

125 ) for emoji, users in reactions_map.items() 

126 ], 

127 is_pinned=m.is_pinned, 

128 is_edited=m.is_edited, 

129 created_at=m.created_at, 

130 updated_at=m.created_at, 

131 )) 

132 return result 

133 

134@router.post("/channels/{channel_id}/messages", auth=JWTAuth(), response={201: MessageOut}) 

135def send_message(request, channel_id: UUID, payload: MessageSendIn): 

136 tenant_id = require_tenant(request) 

137 msg = ChatService.send_message(tenant_id, request.user, channel_id, payload) 

138 return 201, MessageOut( 

139 id=msg.id, 

140 channel_id=msg.channel_id, 

141 sender_id=msg.sender.id, 

142 sender_name=msg.sender.full_name, 

143 sender_avatar=msg.sender.avatar_url, 

144 content=msg.content, 

145 parent_id=msg.parent_id, 

146 attachments=[], 

147 reactions=[], 

148 is_pinned=msg.is_pinned, 

149 is_edited=msg.is_edited, 

150 created_at=msg.created_at, 

151 updated_at=msg.created_at, 

152 )