update for adding multiple members (#25128)
* update for adding multiple members
* fix unit test
* more test fixes
* add another unit test
* fix object passed by client4
* revert package-lock.json
* revert package-lock.json
* add length check
* limit size of lists in API requests
* revert package-lock
* add batching to front end
* add batching to front end
* fix bad merge
* update return type
* remove unnecessary permisssion check, add unit test
* fixes and add tests from review
* revert changes adding limits to other apis
* fixes
* clean-up from code review
* fix unit test call
* revert back to interface{}, fix unit test
---------
Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
4f68dbb96e
Коммит
6fd894953c
@@ -180,7 +180,9 @@
|
|||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
description: User ids to be in the group message channel
|
minItems: 3
|
||||||
|
maxItems: 8
|
||||||
|
description: User ids to be in the group message channel.
|
||||||
required: true
|
required: true
|
||||||
responses:
|
responses:
|
||||||
"201":
|
"201":
|
||||||
@@ -408,7 +410,7 @@
|
|||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
description: List of channel ids
|
description: List of channel ids.
|
||||||
required: true
|
required: true
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
@@ -1329,8 +1331,8 @@
|
|||||||
post:
|
post:
|
||||||
tags:
|
tags:
|
||||||
- channels
|
- channels
|
||||||
summary: Add user to channel
|
summary: Add user(s) to channel
|
||||||
description: Add a user to a channel by creating a channel member object.
|
description: Add a user(s) to a channel by creating a channel member object(s).
|
||||||
operationId: AddChannelMember
|
operationId: AddChannelMember
|
||||||
parameters:
|
parameters:
|
||||||
- name: channel_id
|
- name: channel_id
|
||||||
@@ -1344,12 +1346,17 @@
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
type: object
|
type: object
|
||||||
required:
|
|
||||||
- user_id
|
|
||||||
properties:
|
properties:
|
||||||
user_id:
|
user_id:
|
||||||
type: string
|
type: string
|
||||||
description: The ID of user to add into the channel
|
description: The ID of user to add into the channel, for backwards compatibility.
|
||||||
|
user_ids:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
minItems: 1
|
||||||
|
maxItems: 1000
|
||||||
|
description: The IDs of users to add into the channel, required if 'user_id' doess not exist.
|
||||||
post_root_id:
|
post_root_id:
|
||||||
type: string
|
type: string
|
||||||
description: The ID of root post where link to add channel member
|
description: The ID of root post where link to add channel member
|
||||||
@@ -1392,7 +1399,7 @@
|
|||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
description: List of user ids
|
description: List of user ids.
|
||||||
required: true
|
required: true
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import (
|
|||||||
"github.com/mattermost/mattermost/server/v8/channels/audit"
|
"github.com/mattermost/mattermost/server/v8/channels/audit"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const maxListSize = 1000
|
||||||
|
|
||||||
func (api *API) InitChannel() {
|
func (api *API) InitChannel() {
|
||||||
api.BaseRoutes.Channels.Handle("", api.APISessionRequired(getAllChannels)).Methods("GET")
|
api.BaseRoutes.Channels.Handle("", api.APISessionRequired(getAllChannels)).Methods("GET")
|
||||||
api.BaseRoutes.Channels.Handle("", api.APISessionRequired(createChannel)).Methods("POST")
|
api.BaseRoutes.Channels.Handle("", api.APISessionRequired(createChannel)).Methods("POST")
|
||||||
@@ -1725,43 +1727,47 @@ func addChannelMember(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
props := model.StringInterfaceFromJSON(r.Body)
|
props := model.StringInterfaceFromJSON(r.Body)
|
||||||
userId, ok := props["user_id"].(string)
|
|
||||||
if !ok || !model.IsValidId(userId) {
|
|
||||||
c.SetInvalidParam("user_id")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
auditRec := c.MakeAuditRecord("addChannelMember", audit.Fail)
|
var userIds []string
|
||||||
defer c.LogAuditRec(auditRec)
|
interfaceIds, ok := props["user_ids"].([]interface{})
|
||||||
audit.AddEventParameter(auditRec, "user_id", userId)
|
if ok {
|
||||||
audit.AddEventParameter(auditRec, "channel_id", c.Params.ChannelId)
|
if len(interfaceIds) > maxListSize {
|
||||||
|
c.SetInvalidParam("user_ids")
|
||||||
member := &model.ChannelMember{
|
return
|
||||||
ChannelId: c.Params.ChannelId,
|
}
|
||||||
UserId: userId,
|
for _, userId := range interfaceIds {
|
||||||
|
userIds = append(userIds, userId.(string))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
userId, ok2 := props["user_id"].(string)
|
||||||
|
if !ok2 || !model.IsValidId(userId) {
|
||||||
|
c.SetInvalidParam("user_id or user_ids")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
userIds = append(userIds, userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
postRootId, ok := props["post_root_id"].(string)
|
postRootId, ok := props["post_root_id"].(string)
|
||||||
if ok && postRootId != "" && !model.IsValidId(postRootId) {
|
if ok && postRootId != "" {
|
||||||
c.SetInvalidParam("post_root_id")
|
if !model.IsValidId(postRootId) {
|
||||||
return
|
c.SetInvalidParam("post_root_id")
|
||||||
}
|
return
|
||||||
|
}
|
||||||
|
|
||||||
audit.AddEventParameter(auditRec, "post_root_id", postRootId)
|
|
||||||
|
|
||||||
if ok && len(postRootId) == 26 {
|
|
||||||
rootPost, err := c.App.GetSinglePost(c.AppContext, postRootId, false)
|
rootPost, err := c.App.GetSinglePost(c.AppContext, postRootId, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Err = err
|
c.Err = err
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if rootPost.ChannelId != member.ChannelId {
|
if rootPost.ChannelId != c.Params.ChannelId {
|
||||||
c.SetInvalidParam("post_root_id")
|
c.SetInvalidParam("post_root_id")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
} else if !ok {
|
||||||
|
postRootId = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
channel, err := c.App.GetChannel(c.AppContext, member.ChannelId)
|
channel, err := c.App.GetChannel(c.AppContext, c.Params.ChannelId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Err = err
|
c.Err = err
|
||||||
return
|
return
|
||||||
@@ -1772,54 +1778,28 @@ func addChannelMember(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
isNewMembership := false
|
canAddSelf := false
|
||||||
if _, err = c.App.GetChannelMember(c.AppContext, member.ChannelId, member.UserId); err != nil {
|
canAddOthers := false
|
||||||
if err.Id == app.MissingChannelMemberError {
|
|
||||||
isNewMembership = true
|
|
||||||
} else {
|
|
||||||
c.Err = err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isSelfAdd := member.UserId == c.AppContext.Session().UserId
|
|
||||||
|
|
||||||
if channel.Type == model.ChannelTypeOpen {
|
if channel.Type == model.ChannelTypeOpen {
|
||||||
if isSelfAdd && isNewMembership {
|
if c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), channel.TeamId, model.PermissionJoinPublicChannels) {
|
||||||
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), channel.TeamId, model.PermissionJoinPublicChannels) {
|
canAddSelf = true
|
||||||
c.SetPermissionError(model.PermissionJoinPublicChannels)
|
}
|
||||||
return
|
if c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channel.Id, model.PermissionManagePublicChannelMembers) {
|
||||||
}
|
canAddOthers = true
|
||||||
} else if isSelfAdd && !isNewMembership {
|
|
||||||
// nothing to do, since already in the channel
|
|
||||||
} else if !isSelfAdd {
|
|
||||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channel.Id, model.PermissionManagePublicChannelMembers) {
|
|
||||||
c.SetPermissionError(model.PermissionManagePublicChannelMembers)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if channel.Type == model.ChannelTypePrivate {
|
if channel.Type == model.ChannelTypePrivate {
|
||||||
if isSelfAdd && isNewMembership {
|
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channel.Id, model.PermissionManagePrivateChannelMembers) {
|
||||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channel.Id, model.PermissionManagePrivateChannelMembers) {
|
c.SetPermissionError(model.PermissionManagePrivateChannelMembers)
|
||||||
c.SetPermissionError(model.PermissionManagePrivateChannelMembers)
|
return
|
||||||
return
|
|
||||||
}
|
|
||||||
} else if isSelfAdd && !isNewMembership {
|
|
||||||
// nothing to do, since already in the channel
|
|
||||||
} else if !isSelfAdd {
|
|
||||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channel.Id, model.PermissionManagePrivateChannelMembers) {
|
|
||||||
c.SetPermissionError(model.PermissionManagePrivateChannelMembers)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if channel.IsGroupConstrained() {
|
if channel.IsGroupConstrained() {
|
||||||
nonMembers, err := c.App.FilterNonGroupChannelMembers([]string{member.UserId}, channel)
|
nonMembers, err := c.App.FilterNonGroupChannelMembers(userIds, channel)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if v, ok := err.(*model.AppError); ok {
|
if v, ok2 := err.(*model.AppError); ok2 {
|
||||||
c.Err = v
|
c.Err = v
|
||||||
} else {
|
} else {
|
||||||
c.Err = model.NewAppError("addChannelMember", "api.channel.add_members.error", nil, "", http.StatusBadRequest).Wrap(err)
|
c.Err = model.NewAppError("addChannelMember", "api.channel.add_members.error", nil, "", http.StatusBadRequest).Wrap(err)
|
||||||
@@ -1832,32 +1812,98 @@ func addChannelMember(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cm, err := c.App.AddChannelMember(c.AppContext, member.UserId, channel, app.ChannelMemberOpts{
|
var lastError *model.AppError
|
||||||
UserRequestorID: c.AppContext.Session().UserId,
|
var newChannelMembers []model.ChannelMember
|
||||||
PostRootID: postRootId,
|
for _, userId := range userIds {
|
||||||
})
|
if !model.IsValidId(userId) {
|
||||||
if err != nil {
|
c.Logger.Warn("Error adding channel member, invalid UserId", mlog.String("UserId", userId), mlog.String("ChannelId", channel.Id))
|
||||||
c.Err = err
|
c.SetInvalidParam("user_id")
|
||||||
|
lastError = c.Err
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
auditRec := c.MakeAuditRecord("addChannelMember", audit.Fail)
|
||||||
|
defer c.LogAuditRec(auditRec)
|
||||||
|
audit.AddEventParameter(auditRec, "user_id", userId)
|
||||||
|
audit.AddEventParameter(auditRec, "channel_id", c.Params.ChannelId)
|
||||||
|
audit.AddEventParameter(auditRec, "post_root_id", postRootId)
|
||||||
|
|
||||||
|
member := &model.ChannelMember{
|
||||||
|
ChannelId: c.Params.ChannelId,
|
||||||
|
UserId: userId,
|
||||||
|
}
|
||||||
|
|
||||||
|
existingMember, err := c.App.GetChannelMember(c.AppContext, member.ChannelId, member.UserId)
|
||||||
|
if err != nil {
|
||||||
|
if err.Id != app.MissingChannelMemberError {
|
||||||
|
c.Logger.Warn("Error adding channel member, error getting channel member", mlog.String("UserId", userId), mlog.String("ChannelId", channel.Id), mlog.Err(err))
|
||||||
|
lastError = err
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// user is already a member, go to next
|
||||||
|
c.Logger.Warn("User is already a channel member, skipping", mlog.String("UserId", userId), mlog.String("ChannelId", channel.Id))
|
||||||
|
newChannelMembers = append(newChannelMembers, *existingMember)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if channel.Type == model.ChannelTypeOpen {
|
||||||
|
isSelfAdd := member.UserId == c.AppContext.Session().UserId
|
||||||
|
if isSelfAdd && !canAddSelf {
|
||||||
|
c.Logger.Warn("Error adding channel member, Invalid Permission to add self", mlog.String("UserId", userId), mlog.String("ChannelId", channel.Id))
|
||||||
|
c.SetPermissionError(model.PermissionJoinPublicChannels)
|
||||||
|
lastError = c.Err
|
||||||
|
continue
|
||||||
|
} else if !isSelfAdd && !canAddOthers {
|
||||||
|
c.Logger.Warn("Error adding channel member, Invalid Permission to add others", mlog.String("UserId", userId), mlog.String("ChannelId", channel.Id))
|
||||||
|
c.SetPermissionError(model.PermissionManagePublicChannelMembers)
|
||||||
|
lastError = c.Err
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cm, err := c.App.AddChannelMember(c.AppContext, member.UserId, channel, app.ChannelMemberOpts{
|
||||||
|
UserRequestorID: c.AppContext.Session().UserId,
|
||||||
|
PostRootID: postRootId,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
c.Logger.Warn("Error adding channel member", mlog.String("UserId", userId), mlog.String("ChannelId", channel.Id), mlog.Err(err))
|
||||||
|
lastError = err
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
newChannelMembers = append(newChannelMembers, *cm)
|
||||||
|
|
||||||
|
if postRootId != "" {
|
||||||
|
err := c.App.UpdateThreadFollowForUserFromChannelAdd(c.AppContext, cm.UserId, channel.TeamId, postRootId)
|
||||||
|
if err != nil {
|
||||||
|
c.Logger.Warn("Error adding channel member, error updating thread", mlog.String("UserId", userId), mlog.String("ChannelId", channel.Id), mlog.Err(err))
|
||||||
|
lastError = err
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auditRec.Success()
|
||||||
|
auditRec.AddEventResultState(cm)
|
||||||
|
auditRec.AddEventObjectType("channel_member")
|
||||||
|
auditRec.AddMeta("add_user_id", cm.UserId)
|
||||||
|
c.LogAudit("name=" + channel.Name + " user_id=" + cm.UserId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if lastError != nil && len(newChannelMembers) == 0 {
|
||||||
|
c.Err = lastError
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if postRootId != "" {
|
|
||||||
err := c.App.UpdateThreadFollowForUserFromChannelAdd(c.AppContext, cm.UserId, channel.TeamId, postRootId)
|
|
||||||
if err != nil {
|
|
||||||
c.Err = err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
auditRec.Success()
|
|
||||||
auditRec.AddEventResultState(cm)
|
|
||||||
auditRec.AddEventObjectType("channel_member")
|
|
||||||
auditRec.AddMeta("add_user_id", cm.UserId)
|
|
||||||
c.LogAudit("name=" + channel.Name + " user_id=" + cm.UserId)
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusCreated)
|
w.WriteHeader(http.StatusCreated)
|
||||||
if err := json.NewEncoder(w).Encode(cm); err != nil {
|
userId, ok := props["user_id"]
|
||||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
if ok && len(newChannelMembers) == 1 && newChannelMembers[0].UserId == userId {
|
||||||
|
if err := json.NewEncoder(w).Encode(newChannelMembers[0]); err != nil {
|
||||||
|
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := json.NewEncoder(w).Encode(newChannelMembers); err != nil {
|
||||||
|
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3294,6 +3294,36 @@ func TestAddChannelMember(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAddChannelMembers(t *testing.T) {
|
||||||
|
th := Setup(t).InitBasic()
|
||||||
|
defer th.TearDown()
|
||||||
|
client := th.Client
|
||||||
|
user := th.BasicUser
|
||||||
|
user2 := th.BasicUser2
|
||||||
|
team := th.BasicTeam
|
||||||
|
publicChannel := th.CreatePublicChannel()
|
||||||
|
privateChannel := th.CreatePrivateChannel()
|
||||||
|
|
||||||
|
user3 := th.CreateUserWithClient(th.SystemAdminClient)
|
||||||
|
_, _, err := th.SystemAdminClient.AddTeamMember(context.Background(), team.Id, user3.Id)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
cm, resp, err := client.AddChannelMembers(context.Background(), publicChannel.Id, "", []string{user.Id, user2.Id, user3.Id})
|
||||||
|
require.NoError(t, err)
|
||||||
|
CheckCreatedStatus(t, resp)
|
||||||
|
require.Equal(t, publicChannel.Id, cm[0].ChannelId, "should have returned exact channel")
|
||||||
|
require.Equal(t, user.Id, cm[0].UserId, "should have returned exact user added to public channel")
|
||||||
|
require.Equal(t, user2.Id, cm[1].UserId, "should have returned exact user added to public channel")
|
||||||
|
require.Equal(t, user3.Id, cm[2].UserId, "should have returned exact user added to public channel")
|
||||||
|
|
||||||
|
cm, _, err = client.AddChannelMembers(context.Background(), privateChannel.Id, "", []string{user.Id, user2.Id, user3.Id})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, privateChannel.Id, cm[0].ChannelId, "should have returned exact channel")
|
||||||
|
require.Equal(t, user.Id, cm[0].UserId, "should have returned exact user added to public channel")
|
||||||
|
require.Equal(t, user2.Id, cm[1].UserId, "should have returned exact user added to public channel")
|
||||||
|
require.Equal(t, user3.Id, cm[2].UserId, "should have returned exact user added to public channel")
|
||||||
|
}
|
||||||
|
|
||||||
func TestAddChannelMemberFromThread(t *testing.T) {
|
func TestAddChannelMemberFromThread(t *testing.T) {
|
||||||
t.Skip("MM-41285")
|
t.Skip("MM-41285")
|
||||||
th := Setup(t).InitBasic()
|
th := Setup(t).InitBasic()
|
||||||
@@ -4784,6 +4814,13 @@ func TestGetChannelsMemberCount(t *testing.T) {
|
|||||||
_, _, err := client.GetChannelsMemberCount(context.Background(), channelIDs)
|
_, _, err := client.GetChannelsMemberCount(context.Background(), channelIDs)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("Should fail for private channels that the user is not a member of", func(t *testing.T) {
|
||||||
|
th.LoginBasic2()
|
||||||
|
channelIDs := []string{channel2.Id}
|
||||||
|
_, _, err := client.GetChannelsMemberCount(context.Background(), channelIDs)
|
||||||
|
require.Error(t, err)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMoveChannel(t *testing.T) {
|
func TestMoveChannel(t *testing.T) {
|
||||||
|
|||||||
@@ -3730,6 +3730,23 @@ func (c *Client4) AddChannelMember(ctx context.Context, channelId, userId string
|
|||||||
return ch, BuildResponse(r), nil
|
return ch, BuildResponse(r), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddChannelMembers adds users to a channel and return an array of channel members.
|
||||||
|
func (c *Client4) AddChannelMembers(ctx context.Context, channelId, postRootId string, userIds []string) ([]*ChannelMember, *Response, error) {
|
||||||
|
requestBody := map[string]any{"user_ids": userIds, "post_root_id": postRootId}
|
||||||
|
r, err := c.DoAPIPost(ctx, c.channelMembersRoute(channelId)+"", StringInterfaceToJSON(requestBody))
|
||||||
|
if err != nil {
|
||||||
|
return nil, BuildResponse(r), err
|
||||||
|
}
|
||||||
|
defer closeBody(r)
|
||||||
|
|
||||||
|
var ch []*ChannelMember
|
||||||
|
err = json.NewDecoder(r.Body).Decode(&ch)
|
||||||
|
if err != nil {
|
||||||
|
return nil, BuildResponse(r), NewAppError("AddChannelMembers", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||||
|
}
|
||||||
|
return ch, BuildResponse(r), nil
|
||||||
|
}
|
||||||
|
|
||||||
// AddChannelMemberWithRootId adds user to channel and return a channel member. Post add to channel message has the postRootId.
|
// AddChannelMemberWithRootId adds user to channel and return a channel member. Post add to channel message has the postRootId.
|
||||||
func (c *Client4) AddChannelMemberWithRootId(ctx context.Context, channelId, userId, postRootId string) (*ChannelMember, *Response, error) {
|
func (c *Client4) AddChannelMemberWithRootId(ctx context.Context, channelId, userId, postRootId string) (*ChannelMember, *Response, error) {
|
||||||
requestBody := map[string]string{"user_id": userId, "post_root_id": postRootId}
|
requestBody := map[string]string{"user_id": userId, "post_root_id": postRootId}
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ jest.mock('mattermost-redux/actions/channels', () => ({
|
|||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
addChannelMember: (...args: any) => ({type: 'MOCK_ADD_CHANNEL_MEMBER', args}),
|
addChannelMembers: (...args: any) => ({type: 'MOCK_ADD_CHANNEL_MEMBERS', args}),
|
||||||
createDirectChannel: (...args: any) => ({type: 'MOCK_CREATE_DIRECT_CHANNEL', args}),
|
createDirectChannel: (...args: any) => ({type: 'MOCK_CREATE_DIRECT_CHANNEL', args}),
|
||||||
createGroupChannel: (...args: any) => ({type: 'MOCK_CREATE_GROUP_CHANNEL', args}),
|
createGroupChannel: (...args: any) => ({type: 'MOCK_CREATE_GROUP_CHANNEL', args}),
|
||||||
}));
|
}));
|
||||||
@@ -146,13 +146,13 @@ describe('Actions.Channel', () => {
|
|||||||
const testStore = await mockStore(initialState);
|
const testStore = await mockStore(initialState);
|
||||||
|
|
||||||
const expectedActions = [{
|
const expectedActions = [{
|
||||||
type: 'MOCK_ADD_CHANNEL_MEMBER',
|
type: 'MOCK_ADD_CHANNEL_MEMBERS',
|
||||||
args: ['testid', 'testuserid'],
|
args: ['testid', ['testuserid', 'testuserid2']],
|
||||||
}];
|
}];
|
||||||
|
|
||||||
const fakeData = {
|
const fakeData = {
|
||||||
channel: 'testid',
|
channel: 'testid',
|
||||||
userIds: ['testuserid'],
|
userIds: ['testuserid', 'testuserid2'],
|
||||||
};
|
};
|
||||||
|
|
||||||
await testStore.dispatch(addUsersToChannel(fakeData.channel, fakeData.userIds));
|
await testStore.dispatch(addUsersToChannel(fakeData.channel, fakeData.userIds));
|
||||||
|
|||||||
@@ -155,15 +155,11 @@ export function autocompleteChannelsForSearch(term: string, success?: (channels:
|
|||||||
|
|
||||||
export function addUsersToChannel(channelId: Channel['id'], userIds: Array<UserProfile['id']>): ActionFuncAsync {
|
export function addUsersToChannel(channelId: Channel['id'], userIds: Array<UserProfile['id']>): ActionFuncAsync {
|
||||||
return async (dispatch) => {
|
return async (dispatch) => {
|
||||||
try {
|
const error = await dispatch(ChannelActions.addChannelMembers(channelId, userIds));
|
||||||
const requests = userIds.map((uId) => dispatch(ChannelActions.addChannelMember(channelId, uId)));
|
if (error) {
|
||||||
|
return error;
|
||||||
await Promise.all(requests);
|
|
||||||
|
|
||||||
return {data: true};
|
|
||||||
} catch (error) {
|
|
||||||
return {error};
|
|
||||||
}
|
}
|
||||||
|
return {data: true};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1347,6 +1347,90 @@ describe('Actions.Channels', () => {
|
|||||||
expect(stats[channelId].member_count >= 2).toBeTruthy();
|
expect(stats[channelId].member_count >= 2).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('addChannelMembers', async () => {
|
||||||
|
const channelId = TestHelper.basicChannel!.id;
|
||||||
|
|
||||||
|
nock(Client4.getBaseRoute()).
|
||||||
|
post(`/channels/${TestHelper.basicChannel!.id}/members`).
|
||||||
|
reply(201, {channel_id: TestHelper.basicChannel!.id, roles: 'channel_user', user_id: TestHelper.basicUser!.id});
|
||||||
|
|
||||||
|
await store.dispatch(Actions.joinChannel(TestHelper.basicUser!.id, TestHelper.basicTeam!.id, channelId));
|
||||||
|
|
||||||
|
nock(Client4.getBaseRoute()).
|
||||||
|
get(`/channels/${TestHelper.basicChannel!.id}/stats?exclude_files_count=true`).
|
||||||
|
reply(200, {channel_id: TestHelper.basicChannel!.id, member_count: 1});
|
||||||
|
|
||||||
|
await store.dispatch(Actions.getChannelStats(channelId));
|
||||||
|
|
||||||
|
let state = store.getState();
|
||||||
|
let {stats} = state.entities.channels;
|
||||||
|
expect(stats).toBeTruthy();
|
||||||
|
|
||||||
|
// stats for channel
|
||||||
|
expect(stats[channelId]).toBeTruthy();
|
||||||
|
|
||||||
|
// member count for channel
|
||||||
|
expect(stats[channelId].member_count).toBeTruthy();
|
||||||
|
|
||||||
|
// incorrect member count for channel
|
||||||
|
expect(stats[channelId].member_count >= 1).toBeTruthy();
|
||||||
|
|
||||||
|
nock(Client4.getBaseRoute()).
|
||||||
|
post('/users').
|
||||||
|
query(true).
|
||||||
|
reply(201, TestHelper.fakeUserWithId());
|
||||||
|
const user = await TestHelper.basicClient4!.createUser(
|
||||||
|
TestHelper.fakeUser(),
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
TestHelper.basicTeam!.invite_id,
|
||||||
|
);
|
||||||
|
|
||||||
|
nock(Client4.getBaseRoute()).
|
||||||
|
post('/users').
|
||||||
|
query(true).
|
||||||
|
reply(201, TestHelper.fakeUserWithId());
|
||||||
|
const user2 = await TestHelper.basicClient4!.createUser(
|
||||||
|
TestHelper.fakeUser(),
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
TestHelper.basicTeam!.invite_id,
|
||||||
|
);
|
||||||
|
|
||||||
|
nock(Client4.getBaseRoute()).
|
||||||
|
post(`/channels/${TestHelper.basicChannel!.id}/members`).
|
||||||
|
reply(201, [{channel_id: TestHelper.basicChannel!.id, roles: 'channel_user', user_id: user.id},
|
||||||
|
{channel_id: TestHelper.basicChannel!.id, roles: 'channel_user', user_id: user2.id}]);
|
||||||
|
|
||||||
|
await store.dispatch(Actions.addChannelMembers(channelId, [user.id, user2.id]));
|
||||||
|
|
||||||
|
state = store.getState();
|
||||||
|
|
||||||
|
const {profilesInChannel, profilesNotInChannel} = state.entities.users;
|
||||||
|
const channel = profilesInChannel[channelId];
|
||||||
|
const notChannel = profilesNotInChannel[channelId];
|
||||||
|
expect(channel).toBeTruthy();
|
||||||
|
expect(notChannel).toBeTruthy();
|
||||||
|
expect(channel.has(user.id)).toBeTruthy();
|
||||||
|
expect(channel.has(user2.id)).toBeTruthy();
|
||||||
|
|
||||||
|
// user should not present in profilesNotInChannel
|
||||||
|
expect(notChannel.has(user.id)).toEqual(false);
|
||||||
|
expect(notChannel.has(user2.id)).toEqual(false);
|
||||||
|
|
||||||
|
stats = state.entities.channels.stats;
|
||||||
|
expect(stats).toBeTruthy();
|
||||||
|
|
||||||
|
// stats for channel
|
||||||
|
expect(stats[channelId]).toBeTruthy();
|
||||||
|
|
||||||
|
// member count for channel
|
||||||
|
expect(stats[channelId].member_count).toBeTruthy();
|
||||||
|
|
||||||
|
// incorrect member count for channel
|
||||||
|
expect(stats[channelId].member_count >= 3).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
it('removeChannelMember', async () => {
|
it('removeChannelMember', async () => {
|
||||||
const channelId = TestHelper.basicChannel!.id;
|
const channelId = TestHelper.basicChannel!.id;
|
||||||
|
|
||||||
|
|||||||
@@ -1048,6 +1048,45 @@ export function addChannelMember(channelId: string, userId: string, postRootId =
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function addChannelMembers(channelId: string, userIds: string[], postRootId = ''): ActionFuncAsync {
|
||||||
|
const batchSize = 1000;
|
||||||
|
return async (dispatch, getState) => {
|
||||||
|
const channelMembers: ChannelMembership[] = [];
|
||||||
|
try {
|
||||||
|
for (let i = 0; i < userIds.length; i += batchSize) {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
const cm = await Client4.addToChannels(userIds.slice(i, i + batchSize), channelId, postRootId);
|
||||||
|
channelMembers.push(...cm);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
forceLogoutIfNecessary(error, dispatch, getState);
|
||||||
|
dispatch(logError(error));
|
||||||
|
return {error};
|
||||||
|
}
|
||||||
|
|
||||||
|
Client4.trackEvent('action', 'action_channels_add_member', {channel_id: channelId});
|
||||||
|
|
||||||
|
const ids = channelMembers.map((member) => ({id: member.user_id}));
|
||||||
|
dispatch(batchActions([
|
||||||
|
{
|
||||||
|
type: UserTypes.RECEIVED_PROFILES_IN_CHANNEL,
|
||||||
|
id: channelId,
|
||||||
|
data: ids,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: ChannelTypes.RECEIVED_CHANNEL_MEMBERS,
|
||||||
|
data: channelMembers,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: ChannelTypes.ADD_CHANNEL_MEMBER_SUCCESS,
|
||||||
|
id: channelId,
|
||||||
|
count: channelMembers.length,
|
||||||
|
},
|
||||||
|
], 'ADD_CHANNEL_MEMBERS.BATCH'));
|
||||||
|
return {data: channelMembers};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function removeChannelMember(channelId: string, userId: string): ActionFuncAsync {
|
export function removeChannelMember(channelId: string, userId: string): ActionFuncAsync {
|
||||||
return async (dispatch, getState) => {
|
return async (dispatch, getState) => {
|
||||||
try {
|
try {
|
||||||
@@ -1400,6 +1439,7 @@ export default {
|
|||||||
searchGroupChannels,
|
searchGroupChannels,
|
||||||
getChannelStats,
|
getChannelStats,
|
||||||
addChannelMember,
|
addChannelMember,
|
||||||
|
addChannelMembers,
|
||||||
removeChannelMember,
|
removeChannelMember,
|
||||||
markChannelAsRead,
|
markChannelAsRead,
|
||||||
favoriteChannel,
|
favoriteChannel,
|
||||||
|
|||||||
@@ -135,6 +135,36 @@ describe('channels', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('ADD_CHANNEL_MEMBER_SUCCESS', () => {
|
||||||
|
const state = deepFreeze(channelsReducer({
|
||||||
|
stats: {
|
||||||
|
channel1: {
|
||||||
|
id: 'channel1',
|
||||||
|
member_count: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, {}));
|
||||||
|
test('should increment by 1 default', () => {
|
||||||
|
const nextState = channelsReducer(state, {
|
||||||
|
type: ChannelTypes.ADD_CHANNEL_MEMBER_SUCCESS,
|
||||||
|
id: 'channel1',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(nextState).not.toBe(state);
|
||||||
|
expect(nextState.stats.channel1.member_count).toEqual(2);
|
||||||
|
});
|
||||||
|
test('should increment by number passed', () => {
|
||||||
|
const nextState = channelsReducer(state, {
|
||||||
|
type: ChannelTypes.ADD_CHANNEL_MEMBER_SUCCESS,
|
||||||
|
id: 'channel1',
|
||||||
|
count: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(nextState).not.toBe(state);
|
||||||
|
expect(nextState.stats.channel1.member_count).toEqual(101);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('REMOVE_MEMBER_FROM_CHANNEL', () => {
|
describe('REMOVE_MEMBER_FROM_CHANNEL', () => {
|
||||||
test('should remove the channel member', () => {
|
test('should remove the channel member', () => {
|
||||||
const state = deepFreeze(channelsReducer({
|
const state = deepFreeze(channelsReducer({
|
||||||
|
|||||||
@@ -592,9 +592,10 @@ function stats(state: RelationOneToOne<Channel, ChannelStats> = {}, action: AnyA
|
|||||||
case ChannelTypes.ADD_CHANNEL_MEMBER_SUCCESS: {
|
case ChannelTypes.ADD_CHANNEL_MEMBER_SUCCESS: {
|
||||||
const nextState = {...state};
|
const nextState = {...state};
|
||||||
const id = action.id;
|
const id = action.id;
|
||||||
|
const receivedCount = action.count ? action.count : 1;
|
||||||
const nextStat = nextState[id];
|
const nextStat = nextState[id];
|
||||||
if (nextStat) {
|
if (nextStat) {
|
||||||
const count = nextStat.member_count + 1;
|
const count = nextStat.member_count + receivedCount;
|
||||||
return {
|
return {
|
||||||
...nextState,
|
...nextState,
|
||||||
[id]: {
|
[id]: {
|
||||||
|
|||||||
@@ -304,6 +304,34 @@ describe('Reducers.users', () => {
|
|||||||
expect(newState.profilesNotInChannel).toEqual(expectedState.profilesNotInChannel);
|
expect(newState.profilesNotInChannel).toEqual(expectedState.profilesNotInChannel);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('UserTypes.RECEIVED_PROFILES_IN_CHANNEL, existing state', () => {
|
||||||
|
const state = {
|
||||||
|
profilesNotInChannel: {
|
||||||
|
id: new Set().add('old_user_id').add('other_user_id'),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const action = {
|
||||||
|
type: UserTypes.RECEIVED_PROFILES_IN_CHANNEL,
|
||||||
|
id: 'id',
|
||||||
|
data: {
|
||||||
|
old_user_id: {
|
||||||
|
id: 'old_user_id',
|
||||||
|
},
|
||||||
|
other_user_id: {
|
||||||
|
id: 'other_user_id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const expectedState = {
|
||||||
|
profilesNotInChannel: {
|
||||||
|
id: new Set(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const newState = reducer(state as unknown as ReducerState, action);
|
||||||
|
expect(newState.profilesNotInChannel).toEqual(expectedState.profilesNotInChannel);
|
||||||
|
});
|
||||||
|
|
||||||
it('UserTypes.RECEIVED_PROFILE_NOT_IN_CHANNEL, no existing profiles', () => {
|
it('UserTypes.RECEIVED_PROFILE_NOT_IN_CHANNEL, no existing profiles', () => {
|
||||||
const state = {
|
const state = {
|
||||||
profilesNotInChannel: {},
|
profilesNotInChannel: {},
|
||||||
|
|||||||
@@ -18,6 +18,12 @@ function profilesToSet(state: RelationOneToManyUnique<Team, UserProfile>, action
|
|||||||
return users.reduce((nextState, user) => addProfileToSet(nextState, id, user.id), state);
|
return users.reduce((nextState, user) => addProfileToSet(nextState, id, user.id), state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function removeProfilesFromSet(state: RelationOneToManyUnique<Team, UserProfile>, action: AnyAction) {
|
||||||
|
const id = action.id;
|
||||||
|
const users: UserProfile[] = Object.values(action.data);
|
||||||
|
return users.reduce((nextState, user) => removeProfileFromSet(nextState, {type: '', data: {id, user_id: user.id}}), state);
|
||||||
|
}
|
||||||
|
|
||||||
function profileListToSet(state: RelationOneToManyUnique<Team, UserProfile>, action: AnyAction, replace = false) {
|
function profileListToSet(state: RelationOneToManyUnique<Team, UserProfile>, action: AnyAction, replace = false) {
|
||||||
const id = action.id;
|
const id = action.id;
|
||||||
const users: UserProfile[] = action.data || [];
|
const users: UserProfile[] = action.data || [];
|
||||||
@@ -391,6 +397,9 @@ function profilesNotInChannel(state: UsersState['profilesNotInChannel'] = {}, ac
|
|||||||
case UserTypes.RECEIVED_PROFILES_NOT_IN_CHANNEL:
|
case UserTypes.RECEIVED_PROFILES_NOT_IN_CHANNEL:
|
||||||
return profilesToSet(state, action);
|
return profilesToSet(state, action);
|
||||||
|
|
||||||
|
case UserTypes.RECEIVED_PROFILES_IN_CHANNEL:
|
||||||
|
return removeProfilesFromSet(state, action);
|
||||||
|
|
||||||
case UserTypes.RECEIVED_PROFILE_IN_CHANNEL:
|
case UserTypes.RECEIVED_PROFILE_IN_CHANNEL:
|
||||||
return removeProfileFromSet(state, action);
|
return removeProfileFromSet(state, action);
|
||||||
|
|
||||||
|
|||||||
@@ -1811,6 +1811,16 @@ export default class Client4 {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
addToChannels = (userIds: string[], channelId: string, postRootId = '') => {
|
||||||
|
this.trackEvent('api', 'api_channels_add_members', {channel_id: channelId});
|
||||||
|
|
||||||
|
const members = {user_ids: userIds, channel_id: channelId, post_root_id: postRootId};
|
||||||
|
return this.doFetch<ChannelMembership[]>(
|
||||||
|
`${this.getChannelMembersRoute(channelId)}`,
|
||||||
|
{method: 'post', body: JSON.stringify(members)},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
addToChannel = (userId: string, channelId: string, postRootId = '') => {
|
addToChannel = (userId: string, channelId: string, postRootId = '') => {
|
||||||
this.trackEvent('api', 'api_channels_add_member', {channel_id: channelId});
|
this.trackEvent('api', 'api_channels_add_member', {channel_id: channelId});
|
||||||
|
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user