feat: add trusted customer service friendships
Deploy im-server production / deploy (push) Successful in 11m38s
Deploy im-server production / deploy (push) Successful in 11m38s
This commit is contained in:
@@ -147,6 +147,7 @@ func (s *testHTTPInfrastructureStore) MarkKafkaDeadLetterReplay(_ types.TenantID
|
||||
type testRelationsStore struct {
|
||||
context *types.RelationshipContext
|
||||
err error
|
||||
directAdded bool
|
||||
created *types.FriendRequest
|
||||
blocked *types.UserBlock
|
||||
deleted bool
|
||||
@@ -964,6 +965,11 @@ func (s *testRelationsStore) CreateFriendRequest(_ types.TenantID, req *types.Fr
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func (s *testRelationsStore) AddFriendDirect(_ types.TenantID, _, _ types.Uid, _ time.Time) error {
|
||||
s.directAdded = true
|
||||
return s.err
|
||||
}
|
||||
|
||||
func (s *testRelationsStore) ListFriendRequests(_ types.TenantID, _ types.Uid, _, _, _ string, _ int) ([]types.FriendRequestListItem, string, error) {
|
||||
return s.friendRequests, "", nil
|
||||
}
|
||||
|
||||
@@ -116,6 +116,8 @@ func serveInternalAdminQuery(wrt http.ResponseWriter, req *http.Request, prefix
|
||||
serveInternalUserCreate(wrt, req, tenant.ID)
|
||||
case req.Method == http.MethodPost && len(parts) == 5 && parts[2] == "users" && parts[4] == "action":
|
||||
serveInternalUserAction(wrt, req, tenant.ID, parts[3])
|
||||
case req.Method == http.MethodPost && len(parts) == 4 && parts[2] == "friends" && parts[3] == "direct":
|
||||
serveInternalFriendDirectAdd(wrt, req, tenant.ID)
|
||||
case req.Method == http.MethodPost && len(parts) == 4 && parts[2] == "login-lockouts" && parts[3] == "release":
|
||||
serveInternalLoginLockoutRelease(wrt, req, tenant.ID)
|
||||
case req.Method == http.MethodGet && len(parts) == 3 && parts[2] == "users":
|
||||
@@ -166,9 +168,37 @@ func serveInternalAdminQuery(wrt http.ResponseWriter, req *http.Request, prefix
|
||||
func isInternalAdminPostRoute(parts []string) bool {
|
||||
return (len(parts) == 3 && parts[2] == "users") ||
|
||||
(len(parts) == 5 && parts[2] == "users" && parts[4] == "action") ||
|
||||
(len(parts) == 4 && parts[2] == "friends" && parts[3] == "direct") ||
|
||||
(len(parts) == 4 && parts[2] == "login-lockouts" && parts[3] == "release")
|
||||
}
|
||||
|
||||
type internalDirectFriendRequest struct {
|
||||
UserID string `json:"user_id"`
|
||||
PeerID string `json:"peer_user_id"`
|
||||
}
|
||||
|
||||
func serveInternalFriendDirectAdd(wrt http.ResponseWriter, req *http.Request, tenantID types.TenantID) {
|
||||
var body internalDirectFriendRequest
|
||||
if err := json.NewDecoder(io.LimitReader(req.Body, 1<<20)).Decode(&body); err != nil {
|
||||
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", nil)
|
||||
return
|
||||
}
|
||||
userID := types.ParseUserId(strings.TrimSpace(body.UserID))
|
||||
peerID := types.ParseUserId(strings.TrimSpace(body.PeerID))
|
||||
if userID.IsZero() || peerID.IsZero() || userID == peerID {
|
||||
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed user IDs", nil)
|
||||
return
|
||||
}
|
||||
if err := store.Relations.AddFriendDirect(tenantID, userID, peerID, time.Now().UTC().Round(time.Millisecond)); err != nil {
|
||||
writeIMRelationError(wrt, req, err)
|
||||
return
|
||||
}
|
||||
writeIMSuccess(wrt, req, map[string]any{
|
||||
"relationship": "friend",
|
||||
"topic": userID.P2PName(peerID),
|
||||
})
|
||||
}
|
||||
|
||||
type internalUserCreateRequest struct {
|
||||
Username string `json:"username"`
|
||||
Nickname string `json:"nickname"`
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -37,6 +38,35 @@ func (internalAdminQueryNotFound) QueryOne(types.TenantID, string, []any) (map[s
|
||||
return nil, types.ErrNotFound
|
||||
}
|
||||
|
||||
func TestServeInternalFriendDirectAddCreatesRelationshipWithoutRequest(t *testing.T) {
|
||||
oldRelations := store.Relations
|
||||
relations := &testRelationsStore{}
|
||||
store.Relations = relations
|
||||
t.Cleanup(func() { store.Relations = oldRelations })
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/internal/admin-query/tenants/default/friends/direct", strings.NewReader(`{"user_id":"usrAQAAAAAAAAA","peer_user_id":"usrAgAAAAAAAAA"}`))
|
||||
wrt := httptest.NewRecorder()
|
||||
serveInternalFriendDirectAdd(wrt, req, types.TenantID(1))
|
||||
|
||||
if wrt.Code != http.StatusOK {
|
||||
t.Fatalf("status: got %d, want %d; body=%s", wrt.Code, http.StatusOK, wrt.Body.String())
|
||||
}
|
||||
if !relations.directAdded || relations.created != nil {
|
||||
t.Fatalf("direct_added=%v friend_request=%v, want direct add and no request", relations.directAdded, relations.created)
|
||||
}
|
||||
var payload struct {
|
||||
Data struct {
|
||||
Relationship string `json:"relationship"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(wrt.Body.Bytes(), &payload); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if payload.Data.Relationship != types.RelationshipFriend {
|
||||
t.Fatalf("relationship = %q, want %q", payload.Data.Relationship, types.RelationshipFriend)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeInternalUserQueryMissingUserReturnsNotFound(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
@@ -3065,6 +3065,7 @@ func validUserProfilePatch(patch types.UserProfilePatch) bool {
|
||||
}
|
||||
|
||||
type RelationsPersistenceInterface interface {
|
||||
AddFriendDirect(tenantID types.TenantID, userID, peerID types.Uid, now time.Time) error
|
||||
CreateFriendRequest(tenantID types.TenantID, req *types.FriendRequest) (*types.FriendRequest, error)
|
||||
ListFriendRequests(tenantID types.TenantID, uid types.Uid, direction, status, cursor string, limit int) ([]types.FriendRequestListItem, string, error)
|
||||
CountPendingFriendRequests(tenantID types.TenantID, uid types.Uid) (int, error)
|
||||
@@ -3153,6 +3154,41 @@ func (relationsMapper) CreateFriendRequest(tenantID types.TenantID, req *types.F
|
||||
return tenantAdp.TenantFriendRequestCreate(tenantID, req)
|
||||
}
|
||||
|
||||
// AddFriendDirect creates a symmetric friendship for trusted server-side
|
||||
// workflows, such as opening a customer-service conversation. It deliberately
|
||||
// does not create a friend-request record, but it still honors either user's
|
||||
// block list.
|
||||
func (relationsMapper) AddFriendDirect(tenantID types.TenantID, userID, peerID types.Uid, now time.Time) error {
|
||||
if userID.IsZero() || peerID.IsZero() || userID == peerID {
|
||||
return types.ErrMalformed
|
||||
}
|
||||
if _, err := Users.Get(tenantID, userID); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := Users.Get(tenantID, peerID); err != nil {
|
||||
return err
|
||||
}
|
||||
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
relationship, err := relationshipContext(tenantAdp, tenantID, userID, peerID, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if relationship.Relationship == types.RelationshipBlockedByMe || relationship.Relationship == types.RelationshipBlockedByPeer {
|
||||
return types.ErrPermissionDenied
|
||||
}
|
||||
if now.IsZero() {
|
||||
now = time.Now().UTC()
|
||||
}
|
||||
if err = tenantAdp.TenantFriendshipUpsertPair(tenantID, userID, peerID, userID.P2PName(peerID), now); err != nil {
|
||||
return err
|
||||
}
|
||||
cacheFriendPair(tenantID, userID, peerID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (relationsMapper) ListFriendRequests(tenantID types.TenantID, uid types.Uid, direction, status, cursor string, limit int) ([]types.FriendRequestListItem, string, error) {
|
||||
if uid.IsZero() {
|
||||
return nil, "", types.ErrMalformed
|
||||
|
||||
Reference in New Issue
Block a user