fix: support removing group members
This commit is contained in:
+40
-1
@@ -4138,7 +4138,7 @@ func serveIMGroupMembers(wrt http.ResponseWriter, req *http.Request, topic strin
|
||||
}
|
||||
|
||||
func serveIMGroupMember(wrt http.ResponseWriter, req *http.Request, topic string, memberID types.Uid) {
|
||||
if req.Method != http.MethodGet {
|
||||
if req.Method != http.MethodGet && req.Method != http.MethodDelete {
|
||||
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30002, imReasonMethodNotAllowed, "method not allowed", nil)
|
||||
return
|
||||
}
|
||||
@@ -4163,6 +4163,20 @@ func serveIMGroupMember(wrt http.ResponseWriter, req *http.Request, topic string
|
||||
writeIMError(wrt, req, http.StatusNotFound, 30004, imReasonMalformedRequest, "member not found", nil)
|
||||
return
|
||||
}
|
||||
if req.Method == http.MethodDelete {
|
||||
if ok, reason := canRemoveIMGroupMember(selfSub, principal.Uid, *member); !ok {
|
||||
writeIMError(wrt, req, http.StatusForbidden, 30005, imReasonPermissionDenied, "permission denied", map[string]any{"user_id": memberID.UserId(), "reason": reason})
|
||||
return
|
||||
}
|
||||
if err := store.GroupBusiness.Leave(principal.TenantID, topic, memberID, time.Now().UTC().Round(time.Millisecond)); err != nil {
|
||||
writeIMRelationError(wrt, req, err)
|
||||
return
|
||||
}
|
||||
publishIMGroupRealtimeEvent(principal.TenantID, topic, principal.Uid, "group.member.left",
|
||||
map[string]any{"user_id": memberID.UserId(), "removed_by": principal.Uid.UserId(), "member_sync": "single"})
|
||||
writeIMSuccess(wrt, req, map[string]any{"topic": topic, "user_id": memberID.UserId(), "status": "left"})
|
||||
return
|
||||
}
|
||||
memberSub, err := store.Subs.Get(principal.TenantID, topic, memberID, false)
|
||||
if err != nil {
|
||||
writeIMRelationError(wrt, req, err)
|
||||
@@ -7111,6 +7125,31 @@ func canMuteIMGroupMember(tenantID types.TenantID, topic string, selfSub *types.
|
||||
return true, ""
|
||||
}
|
||||
|
||||
func canRemoveIMGroupMember(selfSub *types.Subscription, actor types.Uid, target types.GroupMember) (bool, string) {
|
||||
if actor.IsZero() || target.UserID.IsZero() {
|
||||
return false, "invalid_user"
|
||||
}
|
||||
if actor == target.UserID {
|
||||
return false, "self"
|
||||
}
|
||||
actorAccess := types.ModeNone
|
||||
if selfSub != nil {
|
||||
actorAccess = selfSub.ModeGiven & selfSub.ModeWant
|
||||
}
|
||||
if !actorAccess.IsAdmin() {
|
||||
return false, "not_admin"
|
||||
}
|
||||
switch groupMemberRole(groupMemberAccess(target)) {
|
||||
case "owner":
|
||||
return false, "owner"
|
||||
case "admin":
|
||||
if !actorAccess.IsOwner() {
|
||||
return false, "admin"
|
||||
}
|
||||
}
|
||||
return true, ""
|
||||
}
|
||||
|
||||
func groupMemberProfilePayload(topic string, sub *types.Subscription, user *types.User, ctx *types.RelationshipContext) map[string]any {
|
||||
data := groupMemberPayload(*sub, true, ctx.Remark)
|
||||
data["topic"] = topic
|
||||
|
||||
+44
-1
@@ -283,6 +283,8 @@ type testGroupBusinessStore struct {
|
||||
updatedSettings types.GroupSettingsPatch
|
||||
updatedMembers []types.GroupMemberPatch
|
||||
updatedRoleMode types.AccessMode
|
||||
leftTopic string
|
||||
leftUID types.Uid
|
||||
updateSettingsErr error
|
||||
updateMemberErr error
|
||||
}
|
||||
@@ -546,7 +548,9 @@ func (s *testGroupBusinessStore) InviteMember(_ types.TenantID, topic string, in
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (s *testGroupBusinessStore) Leave(_ types.TenantID, _ string, _ types.Uid, _ time.Time) error {
|
||||
func (s *testGroupBusinessStore) Leave(_ types.TenantID, topic string, uid types.Uid, _ time.Time) error {
|
||||
s.leftTopic = topic
|
||||
s.leftUID = uid
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -3127,6 +3131,45 @@ func TestServeIMGroupMemberReturnsSingleSnapshot(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeIMGroupMemberDeleteRemovesMember(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
expectIMBearer(t, ctrl, "token-1", 7, 42)
|
||||
oldSubs := store.Subs
|
||||
oldGroupBusiness := store.GroupBusiness
|
||||
subs := mock_store.NewMockSubsPersistenceInterface(ctrl)
|
||||
store.Subs = subs
|
||||
target := types.Uid(101)
|
||||
groupStore := &testGroupBusinessStore{member: &types.GroupMember{
|
||||
ID: 101, Topic: "grpMention", UserID: target, Role: types.GroupMemberRoleMember,
|
||||
MemberStatus: types.GroupMemberStatusActive, Version: 3,
|
||||
}}
|
||||
store.GroupBusiness = groupStore
|
||||
t.Cleanup(func() {
|
||||
store.Subs = oldSubs
|
||||
store.GroupBusiness = oldGroupBusiness
|
||||
})
|
||||
|
||||
ownerSub := &types.Subscription{TenantID: 7, User: types.Uid(42).String(), Topic: "grpMention", ModeWant: types.ModeCFull, ModeGiven: types.ModeCFull}
|
||||
subs.EXPECT().Get(types.TenantID(7), "grpMention", types.Uid(42), false).Return(ownerSub, nil)
|
||||
|
||||
req := httptest.NewRequest(http.MethodDelete, "/v1/im/groups/grpMention/members/"+target.UserId(), nil)
|
||||
req.Header.Set("Authorization", "Bearer token-1")
|
||||
rec := httptest.NewRecorder()
|
||||
serveIMGroupResource(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status: got %d, want %d; body=%s", rec.Code, http.StatusOK, rec.Body.String())
|
||||
}
|
||||
if groupStore.leftTopic != "grpMention" || groupStore.leftUID != target {
|
||||
t.Fatalf("member was not removed: topic=%q uid=%s", groupStore.leftTopic, groupStore.leftUID.UserId())
|
||||
}
|
||||
data := decodeIMHTTPResponse(t, rec).Data.(map[string]any)
|
||||
if data["topic"] != "grpMention" || data["user_id"] != target.UserId() || data["status"] != "left" {
|
||||
t.Fatalf("unexpected delete response: %#v", data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroupMemberPayloadUserIDFormats(t *testing.T) {
|
||||
raw := types.Uid(101)
|
||||
fromRaw := groupMemberPayload(types.Subscription{
|
||||
|
||||
Reference in New Issue
Block a user