feat: add chapter eight call records
This commit is contained in:
+146
-1
@@ -12,9 +12,11 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tinode/chat/server/logs"
|
||||
"github.com/tinode/chat/server/store"
|
||||
"github.com/tinode/chat/server/store/types"
|
||||
jcr "github.com/tinode/jsonco"
|
||||
)
|
||||
@@ -45,6 +47,9 @@ const (
|
||||
constCallMsgMissed = "missed"
|
||||
// Call is declined (the callee hung up before picking up).
|
||||
constCallMsgDeclined = "declined"
|
||||
|
||||
constCallTypeAudio = "audio"
|
||||
constCallTypeVideo = "video"
|
||||
)
|
||||
|
||||
type callConfig struct {
|
||||
@@ -86,10 +91,127 @@ type videoCall struct {
|
||||
content any
|
||||
// Call message content mime type.
|
||||
contentMime any
|
||||
// Product-level call identifier.
|
||||
callID string
|
||||
// Product-level call type: audio or video.
|
||||
callType string
|
||||
// Time when the call was accepted.
|
||||
acceptedAt time.Time
|
||||
}
|
||||
|
||||
func normalizeCallInvite(head map[string]any, content any) (map[string]any, any, error) {
|
||||
if head == nil {
|
||||
return nil, nil, types.ErrMalformed
|
||||
}
|
||||
state, _ := head["webrtc"].(string)
|
||||
if state != "started" {
|
||||
return nil, nil, types.ErrMalformed
|
||||
}
|
||||
call, ok := content.(map[string]any)
|
||||
if !ok {
|
||||
if typed, typedOK := content.(types.KVMap); typedOK {
|
||||
call = map[string]any(typed)
|
||||
}
|
||||
}
|
||||
if len(call) == 0 {
|
||||
return nil, nil, types.ErrMalformed
|
||||
}
|
||||
callID, _ := call["callId"].(string)
|
||||
callType, _ := call["callType"].(string)
|
||||
if callType == "" {
|
||||
callType, _ = call["media_type"].(string)
|
||||
}
|
||||
callType = strings.ToLower(strings.TrimSpace(callType))
|
||||
if strings.TrimSpace(callID) == "" || (callType != constCallTypeAudio && callType != constCallTypeVideo) {
|
||||
return nil, nil, types.ErrMalformed
|
||||
}
|
||||
normalizedHead := make(map[string]any, len(head)+2)
|
||||
for key, value := range head {
|
||||
normalizedHead[key] = value
|
||||
}
|
||||
normalizedHead["webrtc"] = "started"
|
||||
normalizedHead["x-im-type"] = "call"
|
||||
if _, ok := normalizedHead["mime"]; !ok {
|
||||
normalizedHead["mime"] = "application/json"
|
||||
}
|
||||
normalizedContent := make(map[string]any, len(call)+3)
|
||||
for key, value := range call {
|
||||
normalizedContent[key] = value
|
||||
}
|
||||
normalizedContent["callId"] = strings.TrimSpace(callID)
|
||||
normalizedContent["callType"] = callType
|
||||
normalizedContent["audio"] = true
|
||||
normalizedContent["video"] = callType == constCallTypeVideo
|
||||
return normalizedHead, normalizedContent, nil
|
||||
}
|
||||
|
||||
func callPayloadString(payload any, key string) string {
|
||||
data, ok := payload.(map[string]any)
|
||||
if !ok {
|
||||
if typed, typedOK := payload.(types.KVMap); typedOK {
|
||||
data = map[string]any(typed)
|
||||
}
|
||||
}
|
||||
if len(data) == 0 {
|
||||
return ""
|
||||
}
|
||||
value, _ := data[key].(string)
|
||||
return strings.TrimSpace(value)
|
||||
}
|
||||
|
||||
func callContentString(content any, key string) string {
|
||||
return callPayloadString(content, key)
|
||||
}
|
||||
|
||||
func (call *videoCall) matchesPayload(payload any) bool {
|
||||
payloadCallID := callPayloadString(payload, "callId")
|
||||
return payloadCallID == "" || payloadCallID == call.callID
|
||||
}
|
||||
|
||||
func (t *Topic) auditCallCreate(call *videoCall, caller types.Uid) {
|
||||
if store.Calls == nil || call == nil || call.callID == "" || caller.IsZero() {
|
||||
return
|
||||
}
|
||||
callee := t.p2pOtherUser(caller)
|
||||
if callee.IsZero() {
|
||||
return
|
||||
}
|
||||
now := types.TimeNow()
|
||||
if err := store.Calls.Create(t.tenantID, &types.CallRecord{
|
||||
TenantID: t.tenantID,
|
||||
CallID: call.callID,
|
||||
Topic: t.name,
|
||||
SeqID: call.seq,
|
||||
CallerID: caller,
|
||||
CalleeID: callee,
|
||||
CallType: call.callType,
|
||||
Status: types.CallStatusStarted,
|
||||
StartedAt: now,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}); err != nil {
|
||||
logs.Warn.Printf("topic[%s]: failed to audit call start %s: %v", t.name, call.callID, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Topic) auditCallAccept(call *videoCall, acceptedAt time.Time) {
|
||||
if store.Calls == nil || call == nil || call.callID == "" {
|
||||
return
|
||||
}
|
||||
if err := store.Calls.Accept(t.tenantID, call.callID, call.seq, acceptedAt); err != nil {
|
||||
logs.Warn.Printf("topic[%s]: failed to audit call accept %s: %v", t.name, call.callID, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Topic) auditCallFinish(call *videoCall, status string, durationMS int64, finishedAt time.Time) {
|
||||
if store.Calls == nil || call == nil || call.callID == "" {
|
||||
return
|
||||
}
|
||||
if err := store.Calls.Finish(t.tenantID, call.callID, call.seq, status, durationMS, finishedAt); err != nil {
|
||||
logs.Warn.Printf("topic[%s]: failed to audit call finish %s: %v", t.name, call.callID, err)
|
||||
}
|
||||
}
|
||||
|
||||
// callPartySession returns a session to be stored in the call party data.
|
||||
func callPartySession(sess *Session) *Session {
|
||||
if sess.isProxy() {
|
||||
@@ -226,12 +348,15 @@ func (t *Topic) handleCallInvite(msg *ClientComMessage, asUid types.Uid) {
|
||||
seq: t.lastID,
|
||||
content: msg.Pub.Content,
|
||||
contentMime: msg.Pub.Head["mime"],
|
||||
callID: callContentString(msg.Pub.Content, "callId"),
|
||||
callType: callContentString(msg.Pub.Content, "callType"),
|
||||
}
|
||||
t.currentCall.parties[msg.sess.sid] = callPartyData{
|
||||
uid: asUid,
|
||||
isOriginator: true,
|
||||
sess: callPartySession(msg.sess),
|
||||
}
|
||||
t.auditCallCreate(t.currentCall, asUid)
|
||||
// Wait for constCallEstablishmentTimeout for the other side to accept the call.
|
||||
t.callEstablishmentTimer.Reset(time.Duration(globals.callEstablishmentTimeout) * time.Second)
|
||||
}
|
||||
@@ -255,6 +380,10 @@ func (t *Topic) handleCallEvent(msg *ClientComMessage) {
|
||||
logs.Info.Printf("topic[%s]: invalid seq id - current call (%d) vs received (%d)", t.name, t.currentCall.seq, call.SeqId)
|
||||
return
|
||||
}
|
||||
if !t.currentCall.matchesPayload(call.Payload) {
|
||||
logs.Info.Printf("topic[%s]: invalid call id for seq %d", t.name, t.currentCall.seq)
|
||||
return
|
||||
}
|
||||
|
||||
asUid := types.ParseUserId(msg.AsUser)
|
||||
|
||||
@@ -307,6 +436,7 @@ func (t *Topic) handleCallEvent(msg *ClientComMessage) {
|
||||
sess: callPartySession(msg.sess),
|
||||
}
|
||||
t.currentCall.acceptedAt = time.Now()
|
||||
t.auditCallAccept(t.currentCall, t.currentCall.acceptedAt)
|
||||
|
||||
// Notify other clients that the call has been accepted.
|
||||
t.infoCallSubsOffline(msg.AsUser, asUid, call.Event, t.currentCall.seq, call.Payload, msg.sess.sid, false)
|
||||
@@ -382,10 +512,11 @@ func (t *Topic) maybeEndCallInProgress(from string, msg *ClientComMessage, callD
|
||||
originatorUid, _ := t.getCallOriginator()
|
||||
var replaceWith string
|
||||
var callDuration int64
|
||||
finishedAt := time.Now()
|
||||
if from != "" && len(t.currentCall.parties) == 2 {
|
||||
// This is a call in progress.
|
||||
replaceWith = constCallMsgFinished
|
||||
callDuration = time.Since(t.currentCall.acceptedAt).Milliseconds()
|
||||
callDuration = finishedAt.Sub(t.currentCall.acceptedAt).Milliseconds()
|
||||
} else {
|
||||
if from != "" {
|
||||
// User originated hang-up.
|
||||
@@ -418,6 +549,7 @@ func (t *Topic) maybeEndCallInProgress(from string, msg *ClientComMessage, callD
|
||||
if err := t.saveAndBroadcastMessage(&msgCopy, originatorUid, false, nil, head, t.currentCall.content); err != nil {
|
||||
logs.Err.Printf("topic[%s]: failed to write finalizing message for call seq id %d - '%s'", t.name, t.currentCall.seq, err)
|
||||
}
|
||||
t.auditCallFinish(t.currentCall, callFinalStatus(replaceWith), callDuration, finishedAt)
|
||||
|
||||
// Send {info} hangup event to the subscribed sessions.
|
||||
t.broadcastToSessions(t.currentCall.infoMessage(constCallEventHangUp))
|
||||
@@ -429,6 +561,19 @@ func (t *Topic) maybeEndCallInProgress(from string, msg *ClientComMessage, callD
|
||||
t.currentCall = nil
|
||||
}
|
||||
|
||||
func callFinalStatus(replaceWith string) string {
|
||||
switch replaceWith {
|
||||
case constCallMsgFinished:
|
||||
return types.CallStatusFinished
|
||||
case constCallMsgMissed:
|
||||
return types.CallStatusMissed
|
||||
case constCallMsgDeclined:
|
||||
return types.CallStatusDeclined
|
||||
default:
|
||||
return types.CallStatusDisconnected
|
||||
}
|
||||
}
|
||||
|
||||
// Server initiated call termination.
|
||||
func (t *Topic) terminateCallInProgress(callDidTimeout bool) {
|
||||
if t.currentCall == nil {
|
||||
|
||||
Reference in New Issue
Block a user