feat(call): enhance call handling with error differentiation and online notifications
All checks were successful
Build Backend / build (push) Successful in 16m14s
Build Backend / build-docker (push) Successful in 1m5s

- Added error handling for call invite and answer processes to differentiate between callee offline and call already answered scenarios.
- Introduced a new method to publish messages only to online users, improving real-time communication during call signaling.
- Updated call service to check callee's online status before sending call invites, ensuring better user experience.
- Enhanced call acceptance logic to notify both caller and callee about call status changes, including when a call is answered on another device.
This commit is contained in:
lafay
2026-03-27 17:10:30 +08:00
parent 7e6a65d29d
commit 20b1e04764
3 changed files with 97 additions and 15 deletions

View File

@@ -14,11 +14,19 @@ import (
)
var (
ErrCallNotFound = errors.New("call not found")
ErrCallNotActive = errors.New("call is not active")
ErrCallInProgress = errors.New("call already in progress between users")
ErrNotParticipant = errors.New("user is not a participant of this call")
ErrInvalidCallState = errors.New("invalid call state for this operation")
ErrCallNotFound = errors.New("call not found")
ErrCallNotActive = errors.New("call is not active")
ErrCallInProgress = errors.New("call already in progress between users")
ErrNotParticipant = errors.New("user is not a participant of this call")
ErrInvalidCallState = errors.New("invalid call state for this operation")
ErrCalleeOffline = errors.New("callee is offline")
ErrCallAlreadyAnswered = errors.New("call already answered on another device")
)
// 通话相关常量
const (
CallLifetimeMs = 60000 // 通话邀请有效期 60秒 (参考 Matrix)
CallTimeoutMs = 120000 // 通话超时(无人接听) 120秒
)
// CallService 通话服务接口
@@ -54,6 +62,11 @@ func NewCallService(
}
func (s *callService) Invite(ctx context.Context, callerID, calleeID, conversationID string) (*model.CallSession, error) {
// === Telegram: 检查被叫方是否在线 ===
if !s.hub.HasClients(calleeID) {
return nil, ErrCalleeOffline
}
active, err := s.callRepo.GetActiveCallByConversationID(conversationID)
if err == nil && active != nil {
return nil, fmt.Errorf("%w: call %s", ErrCallInProgress, active.ID)
@@ -83,15 +96,26 @@ func (s *callService) Invite(ctx context.Context, callerID, calleeID, conversati
return nil, fmt.Errorf("get call with participants: %w", err)
}
// === Element: 包含 lifetime 字段 ===
payload := map[string]interface{}{
"call_id": call.ID,
"conversation_id": conversationID,
"caller_id": callerID,
"call_type": call.CallType,
"created_at": now.UnixMilli(),
"lifetime": CallLifetimeMs, // 新增: 有效期 60秒
"ice_servers": s.config.WebRTC.ICEServers,
}
s.hub.PublishToUser(calleeID, "call_incoming", payload)
// === Telegram: 不存 history ===
online := s.hub.PublishToUserOnline(calleeID, "call_incoming", payload)
if !online {
// 极端情况: 检查时在线但发送时离线了,清理通话记录
call.Status = model.CallStatusCancelled
call.EndedAt = &now
s.callRepo.UpdateCall(call)
return nil, ErrCalleeOffline
}
return call, nil
}
@@ -101,8 +125,9 @@ func (s *callService) Accept(ctx context.Context, callID, userID string) (*model
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrCallNotFound, err)
}
if !call.IsActive() {
return nil, ErrCallNotActive
// === 状态原子检查: 确保单设备接听 ===
if call.Status != model.CallStatusCalling {
return nil, ErrCallAlreadyAnswered
}
if !isParticipant(call, userID) {
return nil, ErrNotParticipant
@@ -124,12 +149,19 @@ func (s *callService) Accept(ctx context.Context, callID, userID string) (*model
s.callRepo.UpdateCallParticipant(participant)
}
s.hub.PublishToUser(call.CallerID, "call_accepted", map[string]interface{}{
// === Telegram: 通知拨打方已接听 ===
s.hub.PublishToUserOnline(call.CallerID, "call_accepted", map[string]interface{}{
"call_id": callID,
"started_at": now.UnixMilli(),
"started_at": now.UnixMilli(),
"ice_servers": s.config.WebRTC.ICEServers,
})
// === Telegram: 通知被叫方其他设备 call_answered_elsewhere ===
s.hub.PublishToUserOnline(userID, "call_answered_elsewhere", map[string]interface{}{
"call_id": callID,
"reason": "answered_on_another_device",
})
return call, nil
}
@@ -161,7 +193,7 @@ func (s *callService) Reject(ctx context.Context, callID, userID string) error {
s.callRepo.UpdateCallParticipant(participant)
}
s.hub.PublishToUser(call.CallerID, "call_rejected", map[string]interface{}{
s.hub.PublishToUserOnline(call.CallerID, "call_rejected", map[string]interface{}{
"call_id": callID,
"reason": "rejected",
})
@@ -189,7 +221,7 @@ func (s *callService) Busy(ctx context.Context, callID, userID string) error {
return fmt.Errorf("update call missed: %w", err)
}
s.hub.PublishToUser(call.CallerID, "call_busy", map[string]interface{}{
s.hub.PublishToUserOnline(call.CallerID, "call_busy", map[string]interface{}{
"call_id": callID,
})
return nil
@@ -237,7 +269,7 @@ func (s *callService) End(ctx context.Context, callID, userID string, reason str
participants, _ := s.callRepo.GetCallParticipants(callID)
for _, p := range participants {
if p.UserID != userID {
s.hub.PublishToUser(p.UserID, "call_ended", map[string]interface{}{
s.hub.PublishToUserOnline(p.UserID, "call_ended", map[string]interface{}{
"call_id": callID,
"ended_by": userID,
"reason": reason,
@@ -265,7 +297,7 @@ func (s *callService) RelaySignal(ctx context.Context, callID, fromUserID string
participants, _ := s.callRepo.GetCallParticipants(callID)
for _, p := range participants {
if p.UserID != fromUserID {
s.hub.PublishToUser(p.UserID, signalType, map[string]interface{}{
s.hub.PublishToUserOnline(p.UserID, signalType, map[string]interface{}{
"call_id": callID,
"from_id": fromUserID,
"payload": json.RawMessage(payload),
@@ -290,7 +322,7 @@ func (s *callService) SetMuted(ctx context.Context, callID, userID string, muted
participants, _ := s.callRepo.GetCallParticipants(callID)
for _, p := range participants {
if p.UserID != userID {
s.hub.PublishToUser(p.UserID, "call_peer_muted", map[string]interface{}{
s.hub.PublishToUserOnline(p.UserID, "call_peer_muted", map[string]interface{}{
"call_id": callID,
"user_id": userID,
"muted": muted,