feat: sync recent call and channel fixes
This commit is contained in:
parent
e5e0080216
commit
866a87583e
65 changed files with 6680 additions and 229 deletions
|
|
@ -53,6 +53,15 @@ func (c *participantsReadModelCache) getOrLoad(ctx context.Context, key particip
|
|||
return c.cache.GetOrLoadVersioned(ctx, key, hash, load)
|
||||
}
|
||||
|
||||
func (c *participantsReadModelCache) invalidateChannel(channelID int64) {
|
||||
if c == nil || channelID == 0 {
|
||||
return
|
||||
}
|
||||
c.cache.InvalidateWhere(func(key participantsCacheKey) bool {
|
||||
return key.channelID == channelID
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) cachedParticipants(ctx context.Context, userID, channelID int64, filter domain.ChannelParticipantsFilter, offset, limit int) (domain.ChannelParticipantList, error) {
|
||||
filter, offset, limit = normalizeParticipantsRequest(filter, offset, limit)
|
||||
if s.participantCache == nil || s.versions == nil {
|
||||
|
|
|
|||
|
|
@ -237,6 +237,7 @@ func (s *Service) InviteToChannel(ctx context.Context, userID, channelID int64,
|
|||
res, err := s.channels.InviteToChannel(ctx, channelID, userID, userIDs, date)
|
||||
if err == nil {
|
||||
s.invalidateActiveChannelIDs(activeMembershipUserIDsFromMembers(0, res.Members)...)
|
||||
s.participantCache.invalidateChannel(channelID)
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
|
|
@ -249,6 +250,7 @@ func (s *Service) JoinChannel(ctx context.Context, userID, channelID int64, date
|
|||
res, err := s.channels.JoinChannel(ctx, channelID, userID, date)
|
||||
if err == nil {
|
||||
s.invalidateActiveChannelIDs(userID)
|
||||
s.participantCache.invalidateChannel(channelID)
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
|
|
@ -261,6 +263,7 @@ func (s *Service) LeaveChannel(ctx context.Context, userID, channelID int64, dat
|
|||
res, err := s.channels.LeaveChannel(ctx, channelID, userID, date)
|
||||
if err == nil {
|
||||
s.invalidateActiveChannelIDs(userID)
|
||||
s.participantCache.invalidateChannel(channelID)
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
|
|
@ -318,7 +321,31 @@ func (s *Service) EditAdmin(ctx context.Context, userID int64, req domain.EditCh
|
|||
if req.UserID != userID || req.ChannelID == 0 || req.MemberID == 0 || len(req.Rank) > domain.MaxChannelAdminRankLength {
|
||||
return domain.EditChannelAdminResult{}, domain.ErrChannelInvalid
|
||||
}
|
||||
return s.channels.EditChannelAdmin(ctx, req)
|
||||
res, err := s.channels.EditChannelAdmin(ctx, req)
|
||||
if err == nil {
|
||||
s.invalidateActiveChannelIDs(req.MemberID)
|
||||
s.participantCache.invalidateChannel(req.ChannelID)
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
|
||||
// TransferOwnership transfers a channel/supergroup to another active member.
|
||||
func (s *Service) TransferOwnership(ctx context.Context, userID int64, req domain.TransferChannelOwnershipRequest) (domain.TransferChannelOwnershipResult, error) {
|
||||
if s == nil || s.channels == nil || userID == 0 {
|
||||
return domain.TransferChannelOwnershipResult{}, domain.ErrChannelInvalid
|
||||
}
|
||||
if req.UserID == 0 {
|
||||
req.UserID = userID
|
||||
}
|
||||
if req.UserID != userID || req.ChannelID == 0 || req.NewOwnerID == 0 || req.NewOwnerID == userID {
|
||||
return domain.TransferChannelOwnershipResult{}, domain.ErrChannelInvalid
|
||||
}
|
||||
res, err := s.channels.TransferChannelOwnership(ctx, req)
|
||||
if err == nil {
|
||||
s.invalidateActiveChannelIDs(req.UserID, req.NewOwnerID)
|
||||
s.participantCache.invalidateChannel(req.ChannelID)
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
|
||||
// EditMemberRank sets or clears a participant's member tag without touching
|
||||
|
|
@ -333,7 +360,11 @@ func (s *Service) EditMemberRank(ctx context.Context, userID int64, req domain.E
|
|||
if req.UserID != userID || req.ChannelID == 0 || req.MemberID == 0 || len(req.Rank) > domain.MaxChannelAdminRankLength {
|
||||
return domain.EditChannelAdminResult{}, domain.ErrChannelInvalid
|
||||
}
|
||||
return s.channels.EditChannelMemberRank(ctx, req)
|
||||
res, err := s.channels.EditChannelMemberRank(ctx, req)
|
||||
if err == nil {
|
||||
s.participantCache.invalidateChannel(req.ChannelID)
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
|
||||
// EditBanned edits a participant's banned rights.
|
||||
|
|
@ -350,6 +381,7 @@ func (s *Service) EditBanned(ctx context.Context, userID int64, req domain.EditC
|
|||
res, err := s.channels.EditChannelBanned(ctx, req)
|
||||
if err == nil {
|
||||
s.invalidateActiveChannelIDs(req.Participant.ID)
|
||||
s.participantCache.invalidateChannel(req.ChannelID)
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -568,6 +568,106 @@ func TestGetParticipantsCachesPageByCompositeReadModelHash(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestGetParticipantsCacheInvalidatesAfterAdminMutation(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
const ownerID int64 = 1001
|
||||
base := &countingChannelStore{ChannelStore: memory.NewChannelStore()}
|
||||
service := NewService(base)
|
||||
created, err := service.CreateChannel(ctx, ownerID, domain.CreateChannelRequest{
|
||||
Title: "Admin Cache",
|
||||
Megagroup: true,
|
||||
MemberUserIDs: []int64{1002},
|
||||
Date: 1700004103,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateChannel: %v", err)
|
||||
}
|
||||
peer := domain.Peer{Type: domain.PeerTypeChannel, ID: created.Channel.ID}
|
||||
versions := &fakeReadModelVersions{hashes: map[store.ReadModelKey]int64{
|
||||
{Model: readmodel.ModelChannelBase, OwnerUserID: 0, PeerType: peer.Type, PeerID: peer.ID}: 201,
|
||||
{Model: readmodel.ModelChannelParticipants, OwnerUserID: 0, PeerType: peer.Type, PeerID: peer.ID}: 202,
|
||||
{Model: readmodel.ModelChannelMember, OwnerUserID: ownerID, PeerType: peer.Type, PeerID: peer.ID}: 203,
|
||||
{Model: readmodel.ModelContactAccount, OwnerUserID: ownerID, PeerType: domain.PeerTypeUser, PeerID: ownerID}: 204,
|
||||
}}
|
||||
service = NewService(base, WithReadModelVersions(versions))
|
||||
|
||||
filter := domain.ChannelParticipantsFilter{Kind: domain.ChannelParticipantsAdmins}
|
||||
before, err := service.GetParticipants(ctx, ownerID, created.Channel.ID, filter, 0, 20)
|
||||
if err != nil {
|
||||
t.Fatalf("first admins: %v", err)
|
||||
}
|
||||
if len(before.Participants) != 1 || before.Participants[0].UserID != ownerID {
|
||||
t.Fatalf("first admins = %+v, want only creator", before.Participants)
|
||||
}
|
||||
if _, err := service.GetParticipants(ctx, ownerID, created.Channel.ID, filter, 0, 20); err != nil {
|
||||
t.Fatalf("cached admins: %v", err)
|
||||
}
|
||||
if base.getParticipantCalls != 1 {
|
||||
t.Fatalf("GetParticipants calls before mutation = %d, want 1", base.getParticipantCalls)
|
||||
}
|
||||
|
||||
if _, err := service.EditAdmin(ctx, ownerID, domain.EditChannelAdminRequest{
|
||||
ChannelID: created.Channel.ID,
|
||||
MemberID: 1002,
|
||||
AdminRights: domain.ChannelAdminRights{InviteUsers: true},
|
||||
Date: 1700004104,
|
||||
}); err != nil {
|
||||
t.Fatalf("EditAdmin: %v", err)
|
||||
}
|
||||
after, err := service.GetParticipants(ctx, ownerID, created.Channel.ID, filter, 0, 20)
|
||||
if err != nil {
|
||||
t.Fatalf("admins after mutation: %v", err)
|
||||
}
|
||||
if base.getParticipantCalls != 2 {
|
||||
t.Fatalf("GetParticipants calls after mutation = %d, want 2", base.getParticipantCalls)
|
||||
}
|
||||
if len(after.Participants) != 2 || after.Participants[1].UserID != 1002 || after.Participants[1].Role != domain.ChannelRoleAdmin {
|
||||
t.Fatalf("admins after mutation = %+v, want fresh promoted admin", after.Participants)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFullMegagroupAdminGrantFillsManageRanks(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
service := NewService(memory.NewChannelStore())
|
||||
created, err := service.CreateChannel(ctx, 1001, domain.CreateChannelRequest{
|
||||
Title: "Full Admin",
|
||||
Megagroup: true,
|
||||
MemberUserIDs: []int64{1002},
|
||||
Date: 1700004200,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateChannel: %v", err)
|
||||
}
|
||||
rights := domain.ChannelAdminRights{
|
||||
ChangeInfo: true,
|
||||
DeleteMessages: true,
|
||||
BanUsers: true,
|
||||
InviteUsers: true,
|
||||
PinMessages: true,
|
||||
AddAdmins: true,
|
||||
ManageCall: true,
|
||||
}
|
||||
edited, err := service.EditAdmin(ctx, 1001, domain.EditChannelAdminRequest{
|
||||
ChannelID: created.Channel.ID,
|
||||
MemberID: 1002,
|
||||
AdminRights: rights,
|
||||
Date: 1700004201,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("EditAdmin full rights: %v", err)
|
||||
}
|
||||
if !edited.Participant.AdminRights.ManageRanks {
|
||||
t.Fatalf("edited admin rights = %+v, want ManageRanks for full megagroup admin", edited.Participant.AdminRights)
|
||||
}
|
||||
member, err := service.GetParticipant(ctx, 1001, created.Channel.ID, 1002)
|
||||
if err != nil {
|
||||
t.Fatalf("GetParticipant: %v", err)
|
||||
}
|
||||
if !member.AdminRights.ManageRanks {
|
||||
t.Fatalf("stored admin rights = %+v, want ManageRanks", member.AdminRights)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateChatCreatesMegagroupWithChannelPts(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store := memory.NewChannelStore()
|
||||
|
|
@ -1647,6 +1747,60 @@ func TestDeleteParticipantHistoryDeletesOneBoundedSenderPage(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestTransferOwnershipDoesNotAdvanceChannelPts(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
service := NewService(memory.NewChannelStore())
|
||||
created, err := service.CreateMegagroupFromCreateChat(ctx, 1001, domain.CreateChannelRequest{
|
||||
Title: "Transfer",
|
||||
MemberUserIDs: []int64{1002},
|
||||
Date: 10,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateMegagroupFromCreateChat: %v", err)
|
||||
}
|
||||
ptsBeforeTransfer := created.Channel.Pts
|
||||
transfer, err := service.TransferOwnership(ctx, 1001, domain.TransferChannelOwnershipRequest{
|
||||
ChannelID: created.Channel.ID,
|
||||
NewOwnerID: 1002,
|
||||
Date: 11,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("TransferOwnership: %v", err)
|
||||
}
|
||||
if transfer.Channel.CreatorUserID != 1002 || transfer.NewOwner.Role != domain.ChannelRoleCreator || transfer.OldOwner.Role != domain.ChannelRoleAdmin {
|
||||
t.Fatalf("transfer result = %+v, want owner moved to 1002 and old owner admin", transfer)
|
||||
}
|
||||
if transfer.Channel.Pts != ptsBeforeTransfer {
|
||||
t.Fatalf("transfer channel pts = %d, want unchanged %d", transfer.Channel.Pts, ptsBeforeTransfer)
|
||||
}
|
||||
if len(transfer.Events) != 2 {
|
||||
t.Fatalf("transfer events = %+v, want two participant transitions", transfer.Events)
|
||||
}
|
||||
for _, event := range transfer.Events {
|
||||
if event.Type != domain.ChannelUpdateParticipant || event.Pts != 0 || event.PtsCount != 0 {
|
||||
t.Fatalf("transfer event = %+v, want transient participant event", event)
|
||||
}
|
||||
}
|
||||
diffAfterTransfer, err := service.GetDifference(ctx, 1002, domain.ChannelDifferenceRequest{ChannelID: created.Channel.ID, Pts: ptsBeforeTransfer, Limit: 10})
|
||||
if err != nil {
|
||||
t.Fatalf("GetDifference after transfer: %v", err)
|
||||
}
|
||||
if len(diffAfterTransfer.OtherUpdates) != 0 || diffAfterTransfer.Pts != ptsBeforeTransfer {
|
||||
t.Fatalf("diff after transfer = %+v, want no durable participant update", diffAfterTransfer)
|
||||
}
|
||||
oldOwner, err := service.GetParticipant(ctx, 1002, created.Channel.ID, 1001)
|
||||
if err != nil {
|
||||
t.Fatalf("GetParticipant old owner: %v", err)
|
||||
}
|
||||
newOwner, err := service.GetParticipant(ctx, 1002, created.Channel.ID, 1002)
|
||||
if err != nil {
|
||||
t.Fatalf("GetParticipant new owner: %v", err)
|
||||
}
|
||||
if oldOwner.Role != domain.ChannelRoleAdmin || newOwner.Role != domain.ChannelRoleCreator {
|
||||
t.Fatalf("participants after transfer old=%+v new=%+v, want admin/creator", oldOwner, newOwner)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannelAdminTitlePinAndInvite(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
service := NewService(memory.NewChannelStore())
|
||||
|
|
@ -2292,8 +2446,8 @@ func TestPublicChannelSearchAndResolveUsername(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatalf("SearchPublicChannels joined: %v", err)
|
||||
}
|
||||
if len(joined.MyResults) != 1 || joined.MyResults[0].ID != public.ID || len(joined.Results) != 0 {
|
||||
t.Fatalf("joined public search = %+v, want my public channel only", joined)
|
||||
if len(joined.MyResults) != 0 || len(joined.Results) != 0 {
|
||||
t.Fatalf("joined public search = %+v, want no discovery result for active member", joined)
|
||||
}
|
||||
global, err := service.SearchPublicChannels(ctx, 1003, "public", 10)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ package groupcalls
|
|||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
|
||||
|
|
@ -44,10 +45,55 @@ func (s *Service) Create(ctx context.Context, channelID, creatorUserID int64, ti
|
|||
})
|
||||
}
|
||||
|
||||
// CreateConference 分配 id/access_hash/slug 并创建 ad-hoc conference call。
|
||||
func (s *Service) CreateConference(ctx context.Context, creatorUserID, randomID, migratedFromPhoneCallID int64, now int) (domain.GroupCall, error) {
|
||||
for i := 0; i < 8; i++ {
|
||||
id, err := randomPositiveInt64()
|
||||
if err != nil {
|
||||
return domain.GroupCall{}, err
|
||||
}
|
||||
accessHash, err := randomPositiveInt64()
|
||||
if err != nil {
|
||||
return domain.GroupCall{}, err
|
||||
}
|
||||
slug, err := randomSlug()
|
||||
if err != nil {
|
||||
return domain.GroupCall{}, err
|
||||
}
|
||||
call, err := s.store.CreateConferenceCall(ctx, domain.GroupCall{
|
||||
ID: id,
|
||||
AccessHash: accessHash,
|
||||
CreatorUserID: creatorUserID,
|
||||
Kind: domain.GroupCallKindConference,
|
||||
Version: 1,
|
||||
CreatedAt: now,
|
||||
InviteSlug: slug,
|
||||
InviteLink: conferenceInviteLink(slug),
|
||||
RandomID: randomID,
|
||||
MigratedFromPhoneCallID: migratedFromPhoneCallID,
|
||||
})
|
||||
if err == nil {
|
||||
return call, nil
|
||||
}
|
||||
if err != domain.ErrGroupCallInvalid {
|
||||
return domain.GroupCall{}, err
|
||||
}
|
||||
}
|
||||
return domain.GroupCall{}, fmt.Errorf("groupcalls: exhausted conference slug attempts")
|
||||
}
|
||||
|
||||
func (s *Service) Get(ctx context.Context, callID int64) (domain.GroupCall, bool, error) {
|
||||
return s.store.GetGroupCall(ctx, callID)
|
||||
}
|
||||
|
||||
func (s *Service) GetBySlug(ctx context.Context, slug string) (domain.GroupCall, bool, error) {
|
||||
return s.store.GetGroupCallBySlug(ctx, slug)
|
||||
}
|
||||
|
||||
func (s *Service) GetByInviteMessage(ctx context.Context, userID int64, msgID int) (domain.GroupCall, domain.GroupCallInvite, bool, error) {
|
||||
return s.store.GetGroupCallByInviteMessage(ctx, userID, msgID)
|
||||
}
|
||||
|
||||
func (s *Service) Join(ctx context.Context, req domain.JoinGroupCallRequest) (domain.GroupCallMutation, error) {
|
||||
return s.store.JoinGroupCall(ctx, req)
|
||||
}
|
||||
|
|
@ -56,6 +102,10 @@ func (s *Service) Leave(ctx context.Context, callID, userID int64, now int) (dom
|
|||
return s.store.LeaveGroupCall(ctx, callID, userID, now)
|
||||
}
|
||||
|
||||
func (s *Service) RemoveConferenceParticipants(ctx context.Context, req domain.RemoveConferenceCallParticipantsRequest) (domain.RemoveConferenceCallParticipantsResult, error) {
|
||||
return s.store.RemoveConferenceCallParticipants(ctx, req)
|
||||
}
|
||||
|
||||
func (s *Service) Discard(ctx context.Context, callID int64, now int) (domain.GroupCall, []domain.GroupCallParticipant, error) {
|
||||
return s.store.DiscardGroupCall(ctx, callID, now)
|
||||
}
|
||||
|
|
@ -108,6 +158,26 @@ func (s *Service) ParticipantOverride(ctx context.Context, callID, setterUserID,
|
|||
return s.store.GetParticipantOverride(ctx, callID, setterUserID, targetUserID)
|
||||
}
|
||||
|
||||
func (s *Service) CreateConferenceInvite(ctx context.Context, invite domain.GroupCallInvite) (domain.GroupCallInvite, error) {
|
||||
return s.store.CreateConferenceInvite(ctx, invite)
|
||||
}
|
||||
|
||||
func (s *Service) SetConferenceInviteStatus(ctx context.Context, callID, inviteeUserID int64, msgID int, status domain.GroupCallInviteStatus, now int) (domain.GroupCallInvite, bool, error) {
|
||||
return s.store.SetConferenceInviteStatus(ctx, callID, inviteeUserID, msgID, status, now)
|
||||
}
|
||||
|
||||
func (s *Service) ConferenceRecipients(ctx context.Context, callID int64) ([]int64, error) {
|
||||
return s.store.ListConferenceRecipientUserIDs(ctx, callID)
|
||||
}
|
||||
|
||||
func (s *Service) AppendChainBlock(ctx context.Context, block domain.GroupCallChainBlock) (domain.GroupCallChainBlock, error) {
|
||||
return s.store.AppendGroupCallChainBlock(ctx, block)
|
||||
}
|
||||
|
||||
func (s *Service) ChainBlocks(ctx context.Context, callID int64, subChainID, offset, limit int) (domain.GroupCallChainBlockPage, error) {
|
||||
return s.store.ListGroupCallChainBlocks(ctx, callID, subChainID, offset, limit)
|
||||
}
|
||||
|
||||
func randomPositiveInt64() (int64, error) {
|
||||
var buf [8]byte
|
||||
if _, err := rand.Read(buf[:]); err != nil {
|
||||
|
|
@ -119,3 +189,15 @@ func randomPositiveInt64() (int64, error) {
|
|||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func randomSlug() (string, error) {
|
||||
var buf [18]byte
|
||||
if _, err := rand.Read(buf[:]); err != nil {
|
||||
return "", fmt.Errorf("groupcalls: random slug: %w", err)
|
||||
}
|
||||
return base64.RawURLEncoding.EncodeToString(buf[:]), nil
|
||||
}
|
||||
|
||||
func conferenceInviteLink(slug string) string {
|
||||
return "https://telesrv.net/call/" + slug + "?slug=" + slug
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,11 +83,16 @@ func (r *registry) decActiveLocked(userID int64) {
|
|||
|
||||
// markDiscardedLocked 把非终态 entry 迁入终态并更新并发计数。
|
||||
func (r *registry) markDiscardedLocked(e *entry, reason domain.PhoneCallDiscardReason, duration, nowUnix int) {
|
||||
r.markDiscardedWithSlugLocked(e, reason, "", duration, nowUnix)
|
||||
}
|
||||
|
||||
func (r *registry) markDiscardedWithSlugLocked(e *entry, reason domain.PhoneCallDiscardReason, reasonSlug string, duration, nowUnix int) {
|
||||
if e.call.Terminal() {
|
||||
return
|
||||
}
|
||||
e.call.State = domain.PhoneCallStateDiscarded
|
||||
e.call.DiscardReason = reason
|
||||
e.call.DiscardReasonSlug = reasonSlug
|
||||
e.call.Duration = duration
|
||||
e.call.DiscardedAt = nowUnix
|
||||
r.decActiveLocked(e.call.AdminID)
|
||||
|
|
|
|||
|
|
@ -252,6 +252,10 @@ func (s *Service) ConfirmCall(ctx context.Context, userID, callID, accessHash in
|
|||
// DiscardCall 挂断:任意非终态可达,幂等。already=true 表示通话此前已是终态
|
||||
// (双方同时挂断:先到者定 reason,后到者拿快照)。
|
||||
func (s *Service) DiscardCall(ctx context.Context, userID, callID, accessHash int64, reason domain.PhoneCallDiscardReason, duration int) (domain.PhoneCall, bool, error) {
|
||||
return s.DiscardCallWithSlug(ctx, userID, callID, accessHash, reason, "", duration)
|
||||
}
|
||||
|
||||
func (s *Service) DiscardCallWithSlug(ctx context.Context, userID, callID, accessHash int64, reason domain.PhoneCallDiscardReason, reasonSlug string, duration int) (domain.PhoneCall, bool, error) {
|
||||
s.reg.mu.Lock()
|
||||
defer s.reg.mu.Unlock()
|
||||
e, err := s.lookupLocked(callID, accessHash)
|
||||
|
|
@ -267,11 +271,14 @@ func (s *Service) DiscardCall(ctx context.Context, userID, callID, accessHash in
|
|||
if reason == "" {
|
||||
reason = domain.PhoneCallDiscardReasonHangup
|
||||
}
|
||||
if reason != domain.PhoneCallDiscardReasonMigrateConference {
|
||||
reasonSlug = ""
|
||||
}
|
||||
// duration 只在通话真正建立(Confirmed)后才认,防止客户端把振铃时长报成通话时长。
|
||||
if e.call.StartDate == 0 || duration < 0 {
|
||||
duration = 0
|
||||
}
|
||||
s.reg.markDiscardedLocked(e, reason, duration, int(s.clk.Now().Unix()))
|
||||
s.reg.markDiscardedWithSlugLocked(e, reason, reasonSlug, duration, int(s.clk.Now().Unix()))
|
||||
return e.call, false, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue