feat(call): implement group calling and enhance call management
Introduces group calling capabilities, including group invites and participant lifecycle management. Enhances the existing call system with support for media types (voice/video) and improved participant tracking. - Add `GroupInvite` and `ParticipantJoin/Leave` to `CallService`. - Implement WebSocket handlers for `call_group_invite` and `call_participant_join`. - Update `CallSession` model to support group IDs, media types, and tracking who ended the call. - Integrate `GroupService` into `CallService` via dependency injection. - Add error constants for group call limitations and participant capacity. - Update LiveKit webhook handling to process participant leave events. Refactor grade synchronization to remove semester-based filtering in favor of a more flexible user-based approach and improve GPA summary updates.
This commit is contained in:
@@ -62,19 +62,14 @@ func (h *GradeHandler) ListGrades(c *gin.Context) {
|
||||
response.Unauthorized(c, "")
|
||||
return
|
||||
}
|
||||
semester := c.Query("semester")
|
||||
if semester == "" {
|
||||
response.BadRequest(c, "semester is required")
|
||||
return
|
||||
}
|
||||
|
||||
grades, err := h.gradeRepo.ListByUserAndSemester(userID, semester)
|
||||
grades, err := h.gradeRepo.ListByUser(userID)
|
||||
if err != nil {
|
||||
response.HandleError(c, err, "failed to list grades")
|
||||
return
|
||||
}
|
||||
|
||||
gpaSummary, _ := h.gradeRepo.GetGpaSummary(userID, semester)
|
||||
gpaSummary, _ := h.gradeRepo.GetLatestGpaSummary(userID)
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"grades": grades,
|
||||
|
||||
@@ -127,10 +127,21 @@ func (h *LiveKitHandler) HandleWebhook(c *gin.Context) {
|
||||
case "room_finished":
|
||||
h.handleRoomFinished(c.Request.Context(), event)
|
||||
case "participant_left":
|
||||
identity := event.Participant.GetIdentity()
|
||||
roomName := event.Room.GetName()
|
||||
h.logger.Info("participant left room",
|
||||
zap.String("room", event.Room.GetName()),
|
||||
zap.String("identity", event.Participant.GetIdentity()),
|
||||
zap.String("room", roomName),
|
||||
zap.String("identity", identity),
|
||||
)
|
||||
if identity != "" && roomName != "" {
|
||||
if err := h.callService.ParticipantLeave(c.Request.Context(), roomName, identity, "left_livekit"); err != nil {
|
||||
h.logger.Warn("failed to handle participant_left",
|
||||
zap.String("room", roomName),
|
||||
zap.String("identity", identity),
|
||||
zap.Error(err),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
||||
|
||||
@@ -377,6 +377,8 @@ func (h *WSHandler) handleMessage(client *ws.Client, msg *ws.Message) {
|
||||
// 通话信令
|
||||
case "call_invite":
|
||||
h.handleCallInvite(ctx, client, msg.Payload)
|
||||
case "call_group_invite":
|
||||
h.handleCallGroupInvite(ctx, client, msg.Payload)
|
||||
case "call_answer":
|
||||
h.handleCallAnswer(ctx, client, msg.Payload)
|
||||
case "call_reject":
|
||||
@@ -389,6 +391,8 @@ func (h *WSHandler) handleMessage(client *ws.Client, msg *ws.Message) {
|
||||
h.handleCallEnd(ctx, client, msg.Payload)
|
||||
case "call_mute":
|
||||
h.handleCallMute(ctx, client, msg.Payload)
|
||||
case "call_participant_join":
|
||||
h.handleCallParticipantJoin(ctx, client, msg.Payload)
|
||||
default:
|
||||
h.publisher.SendError(client, "unknown_type", fmt.Sprintf("unknown message type: %s", msg.Type))
|
||||
}
|
||||
@@ -648,6 +652,61 @@ func (h *WSHandler) handleCallInvite(ctx context.Context, client *ws.Client, pay
|
||||
}
|
||||
}
|
||||
|
||||
// handleCallGroupInvite 处理群组通话邀请
|
||||
func (h *WSHandler) handleCallGroupInvite(ctx context.Context, client *ws.Client, payload json.RawMessage) {
|
||||
if !h.isVerified(ctx, client) {
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
GroupID string `json:"group_id"`
|
||||
ConversationID string `json:"conversation_id"`
|
||||
MediaType string `json:"call_type"`
|
||||
}
|
||||
if err := json.Unmarshal(payload, &req); err != nil {
|
||||
h.publisher.SendError(client, "parse_error", "invalid call_group_invite format")
|
||||
return
|
||||
}
|
||||
if req.GroupID == "" || req.ConversationID == "" {
|
||||
h.publisher.SendError(client, "invalid_params", "group_id and conversation_id are required")
|
||||
return
|
||||
}
|
||||
|
||||
mediaType := req.MediaType
|
||||
if mediaType != "video" {
|
||||
mediaType = "voice"
|
||||
}
|
||||
|
||||
call, err := h.callService.GroupInvite(ctx, client.UserID, req.GroupID, req.ConversationID, mediaType)
|
||||
if err != nil {
|
||||
zap.L().Warn("Failed to invite group call",
|
||||
zap.String("caller_id", client.UserID),
|
||||
zap.String("group_id", req.GroupID),
|
||||
zap.Error(err),
|
||||
)
|
||||
h.publisher.SendError(client, "call_group_invite_failed", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp := ws.ResponseMessage{
|
||||
EventID: h.publisher.NextID(),
|
||||
Type: "call_invited",
|
||||
TS: time.Now().UnixMilli(),
|
||||
Payload: map[string]any{
|
||||
"call_id": call.ID,
|
||||
"conversation_id": req.ConversationID,
|
||||
"group_id": req.GroupID,
|
||||
"call_type": "group",
|
||||
"lifetime": service.CallLifetimeMs,
|
||||
},
|
||||
}
|
||||
data, _ := json.Marshal(resp)
|
||||
select {
|
||||
case <-client.Quit:
|
||||
case client.Send <- data:
|
||||
}
|
||||
}
|
||||
|
||||
// handleCallAnswer 处理接听
|
||||
func (h *WSHandler) handleCallAnswer(ctx context.Context, client *ws.Client, payload json.RawMessage) {
|
||||
var req struct {
|
||||
@@ -778,3 +837,18 @@ func (h *WSHandler) handleCallMute(ctx context.Context, client *ws.Client, paylo
|
||||
h.publisher.SendError(client, "call_mute_failed", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// handleCallParticipantJoin 处理群组通话参与者加入
|
||||
func (h *WSHandler) handleCallParticipantJoin(ctx context.Context, client *ws.Client, payload json.RawMessage) {
|
||||
var req struct {
|
||||
CallID string `json:"call_id"`
|
||||
}
|
||||
if err := json.Unmarshal(payload, &req); err != nil || req.CallID == "" {
|
||||
h.publisher.SendError(client, "invalid_params", "call_id is required")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.callService.ParticipantJoin(ctx, req.CallID, client.UserID); err != nil {
|
||||
h.publisher.SendError(client, "call_participant_join_failed", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user