diff --git a/internal/imclient/client.go b/internal/imclient/client.go index f14279e..9617ac9 100644 --- a/internal/imclient/client.go +++ b/internal/imclient/client.go @@ -388,6 +388,11 @@ func IsConflict(err error) bool { return errors.As(err, &target) && target.Status == http.StatusConflict } +func IsPermissionDenied(err error) bool { + var target *Error + return errors.As(err, &target) && target.Status == http.StatusForbidden +} + // IsAuthenticationFailed reports whether the IM server rejected user // credentials. Callers may use this to repair credentials which have drifted // from the local encrypted copy, while leaving transport failures untouched. diff --git a/internal/support/service.go b/internal/support/service.go index 56ae2ee..97081ae 100644 --- a/internal/support/service.go +++ b/internal/support/service.go @@ -125,11 +125,24 @@ func (s *Service) OpenSession(ctx context.Context, tenantID, memberID, serviceID if !strings.EqualFold(strings.TrimSpace(imSession.UserID), strings.TrimSpace(identity.IMUserID)) { return nil, ErrIMUnavailable } + var conversation *imclient.Conversation profile, err := s.im.UserProfile(ctx, imSession.Token, agent.IMUserID) if err != nil { - return nil, fmt.Errorf("query customer service relationship: %w", err) + if !imclient.IsPermissionDenied(err) { + return nil, fmt.Errorf("query customer service relationship: %w", err) + } + conversation, err = s.im.EnsureP2P(ctx, imSession.Token, agent.IMUserID, + fmt.Sprintf("p2p:%s:%s", identity.IMUserID, agent.IMUserID)) + if err != nil { + return nil, fmt.Errorf("query customer service relationship: %w", err) + } + } + relationship := "" + if profile != nil { + relationship = strings.TrimSpace(profile.Relationship) + } else { + relationship = strings.TrimSpace(conversation.Relationship) } - relationship := strings.TrimSpace(profile.Relationship) switch relationship { case "friend": case "none", "": @@ -139,13 +152,15 @@ func (s *Service) OpenSession(ctx context.Context, tenantID, memberID, serviceID return nil, fmt.Errorf("create customer service friend request: %w", requestErr) } if requestErr != nil { - profile, err = s.im.UserProfile(ctx, imSession.Token, agent.IMUserID) + conversation, err = s.im.EnsureP2P(ctx, imSession.Token, agent.IMUserID, + fmt.Sprintf("p2p:%s:%s", identity.IMUserID, agent.IMUserID)) if err != nil { return nil, fmt.Errorf("refresh customer service relationship: %w", err) } - relationship = strings.TrimSpace(profile.Relationship) + relationship = strings.TrimSpace(conversation.Relationship) } else { relationship = strings.TrimSpace(request.Status) + conversation = nil } if relationship != "accepted" && relationship != "friend" { return nil, ErrRelationPending @@ -155,10 +170,12 @@ func (s *Service) OpenSession(ctx context.Context, tenantID, memberID, serviceID default: return nil, ErrRelationDenied } - conversation, err := s.im.EnsureP2P(ctx, imSession.Token, agent.IMUserID, - fmt.Sprintf("p2p:%s:%s", identity.IMUserID, agent.IMUserID)) - if err != nil { - return nil, fmt.Errorf("ensure customer service conversation: %w", err) + if conversation == nil { + conversation, err = s.im.EnsureP2P(ctx, imSession.Token, agent.IMUserID, + fmt.Sprintf("p2p:%s:%s", identity.IMUserID, agent.IMUserID)) + if err != nil { + return nil, fmt.Errorf("ensure customer service conversation: %w", err) + } } if strings.TrimSpace(conversation.Topic) == "" || (conversation.Relationship != "" && conversation.Relationship != "friend") || !conversation.CanSend { return nil, ErrRelationDenied diff --git a/internal/support/service_test.go b/internal/support/service_test.go index e8037ee..9c36dee 100644 --- a/internal/support/service_test.go +++ b/internal/support/service_test.go @@ -2,6 +2,7 @@ package support import ( "context" + "errors" "strings" "testing" "time" @@ -40,6 +41,9 @@ type imStub struct { loginAttempts int resetPasswords int loginError error + profileError error + conversation *imclient.Conversation + ensureP2PCalls int } func (i *imStub) Ready() bool { return true } @@ -67,6 +71,9 @@ func (i *imStub) ResetPassword(context.Context, string, string) error { return nil } func (i *imStub) UserProfile(context.Context, string, string) (*imclient.UserProfile, error) { + if i.profileError != nil { + return nil, i.profileError + } return &imclient.UserProfile{UserID: "usr_agent", Relationship: "none"}, nil } func (i *imStub) CreateFriendRequest(context.Context, string, string, string) (*imclient.FriendRequest, error) { @@ -74,6 +81,10 @@ func (i *imStub) CreateFriendRequest(context.Context, string, string, string) (* return &imclient.FriendRequest{RequestID: "frq_1", Status: "accepted"}, nil } func (i *imStub) EnsureP2P(context.Context, string, string, string) (*imclient.Conversation, error) { + i.ensureP2PCalls++ + if i.conversation != nil { + return i.conversation, nil + } return &imclient.Conversation{Topic: "p2p-topic", Relationship: "friend", CanSend: true}, nil } @@ -115,6 +126,27 @@ func TestOpenSessionReturnsSameOriginEndpoints(t *testing.T) { } } +func TestOpenSessionReturnsPendingWhenPrivateProfileHidesPendingRelationship(t *testing.T) { + repository := &repositoryStub{ + agent: &store.CustomerService{ID: 3, Name: "客服01", IMTenantCode: "allrich", IMUserID: "usr_agent", OnlineStatus: 1}, + identity: &store.MemberIMIdentity{TenantID: 0, MemberID: 9, IMTenantCode: "allrich", IMUserID: "usr_member", IMUsername: "member01"}, + } + im := &imStub{ + profileError: &imclient.Error{Status: 403, Reason: "PERMISSION_DENIED"}, + conversation: &imclient.Conversation{Topic: "p2p-topic", Relationship: "pending_outbound", CanSend: false}, + } + service := New(repository, im, config.AuthConfig{FinanceMasterKey: strings.Repeat("k", 32)}) + repository.identity.IMPasswordCipher, _ = service.encryptCredential("password") + + _, err := service.OpenSession(context.Background(), 0, 9, 3, SessionRequest{DeviceID: "web-1", Platform: "web"}) + if !errors.Is(err, ErrRelationPending) { + t.Fatalf("error = %v, want ErrRelationPending", err) + } + if im.ensureP2PCalls != 1 || im.friendRequests != 0 { + t.Fatalf("ensure p2p calls=%d friend requests=%d, want 1 and 0", im.ensureP2PCalls, im.friendRequests) + } +} + func TestOpenSessionRepairsDriftedIMCredential(t *testing.T) { repository := &repositoryStub{ agent: &store.CustomerService{ID: 3, Name: "客服01", IMTenantCode: "allrich", IMUserID: "usr_agent", OnlineStatus: 1},