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 {
|
type testRelationsStore struct {
|
||||||
context *types.RelationshipContext
|
context *types.RelationshipContext
|
||||||
err error
|
err error
|
||||||
|
directAdded bool
|
||||||
created *types.FriendRequest
|
created *types.FriendRequest
|
||||||
blocked *types.UserBlock
|
blocked *types.UserBlock
|
||||||
deleted bool
|
deleted bool
|
||||||
@@ -964,6 +965,11 @@ func (s *testRelationsStore) CreateFriendRequest(_ types.TenantID, req *types.Fr
|
|||||||
return req, nil
|
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) {
|
func (s *testRelationsStore) ListFriendRequests(_ types.TenantID, _ types.Uid, _, _, _ string, _ int) ([]types.FriendRequestListItem, string, error) {
|
||||||
return s.friendRequests, "", nil
|
return s.friendRequests, "", nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -116,6 +116,8 @@ func serveInternalAdminQuery(wrt http.ResponseWriter, req *http.Request, prefix
|
|||||||
serveInternalUserCreate(wrt, req, tenant.ID)
|
serveInternalUserCreate(wrt, req, tenant.ID)
|
||||||
case req.Method == http.MethodPost && len(parts) == 5 && parts[2] == "users" && parts[4] == "action":
|
case req.Method == http.MethodPost && len(parts) == 5 && parts[2] == "users" && parts[4] == "action":
|
||||||
serveInternalUserAction(wrt, req, tenant.ID, parts[3])
|
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":
|
case req.Method == http.MethodPost && len(parts) == 4 && parts[2] == "login-lockouts" && parts[3] == "release":
|
||||||
serveInternalLoginLockoutRelease(wrt, req, tenant.ID)
|
serveInternalLoginLockoutRelease(wrt, req, tenant.ID)
|
||||||
case req.Method == http.MethodGet && len(parts) == 3 && parts[2] == "users":
|
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 {
|
func isInternalAdminPostRoute(parts []string) bool {
|
||||||
return (len(parts) == 3 && parts[2] == "users") ||
|
return (len(parts) == 3 && parts[2] == "users") ||
|
||||||
(len(parts) == 5 && parts[2] == "users" && parts[4] == "action") ||
|
(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")
|
(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 {
|
type internalUserCreateRequest struct {
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Nickname string `json:"nickname"`
|
Nickname string `json:"nickname"`
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -37,6 +38,35 @@ func (internalAdminQueryNotFound) QueryOne(types.TenantID, string, []any) (map[s
|
|||||||
return nil, types.ErrNotFound
|
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) {
|
func TestServeInternalUserQueryMissingUserReturnsNotFound(t *testing.T) {
|
||||||
ctrl := gomock.NewController(t)
|
ctrl := gomock.NewController(t)
|
||||||
defer ctrl.Finish()
|
defer ctrl.Finish()
|
||||||
|
|||||||
@@ -3065,6 +3065,7 @@ func validUserProfilePatch(patch types.UserProfilePatch) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type RelationsPersistenceInterface interface {
|
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)
|
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)
|
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)
|
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)
|
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) {
|
func (relationsMapper) ListFriendRequests(tenantID types.TenantID, uid types.Uid, direction, status, cursor string, limit int) ([]types.FriendRequestListItem, string, error) {
|
||||||
if uid.IsZero() {
|
if uid.IsZero() {
|
||||||
return nil, "", types.ErrMalformed
|
return nil, "", types.ErrMalformed
|
||||||
|
|||||||
Reference in New Issue
Block a user