From d4de6e6120bebe57c4b8805d3a53d156d7a6df0a Mon Sep 17 00:00:00 2001 From: Konstantinos Pittas Date: Wed, 4 Jan 2023 21:49:15 +0200 Subject: [PATCH] [MM-23837] Support multiple users/channels in invite slash command (#21726) * accept multiple users and channels * remove logs * add translation for multiple * refactor * fix lint issues * rollback translations and use multiple messages * fix test * fix spacing * extract permission checking * improve check * improve error messages * rewrite tests for better clarity This way the environment is being rebuild so it starts (almost) fresh without interfering with each other * make errors non-blocking * simplify responses collector Co-authored-by: Mattermod --- app/slashcommands/command_invite.go | 247 +++++++++-------- app/slashcommands/command_invite_test.go | 326 +++++++++++++---------- i18n/en.json | 8 +- 3 files changed, 327 insertions(+), 254 deletions(-) diff --git a/app/slashcommands/command_invite.go b/app/slashcommands/command_invite.go index 3bf4931324..d4abda9054 100644 --- a/app/slashcommands/command_invite.go +++ b/app/slashcommands/command_invite.go @@ -10,7 +10,6 @@ import ( "github.com/mattermost/mattermost-server/v6/app/request" "github.com/mattermost/mattermost-server/v6/model" "github.com/mattermost/mattermost-server/v6/shared/i18n" - "github.com/mattermost/mattermost-server/v6/shared/mlog" ) type InviteProvider struct { @@ -38,137 +37,177 @@ func (*InviteProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Comma } } -func (*InviteProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse { +func (i *InviteProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse { + return &model.CommandResponse{ + Text: i.doCommand(a, c, args, message), + ResponseType: model.CommandResponseTypeEphemeral, + } +} + +func (i *InviteProvider) doCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) string { if message == "" { - return &model.CommandResponse{ - Text: args.T("api.command_invite.missing_message.app_error"), - ResponseType: model.CommandResponseTypeEphemeral, + return args.T("api.command_invite.missing_message.app_error") + } + + resps := &[]string{} + + targetUsers, targetChannels, resp := i.parseMessage(a, c, args, resps, message) + if resp != "" { + return resp + } + + // Verify that the inviter has permissions to invite users to the every channel. + targetChannels = i.checkPermissions(a, c, args, resps, targetUsers[0], targetChannels) + + for _, targetUser := range targetUsers { + for _, targetChannel := range targetChannels { + if resp = i.addUserToChannel(a, c, args, targetUser, targetChannel); resp != "" { + *resps = append(*resps, resp) + continue + } + if args.ChannelId != targetChannel.Id { + *resps = append(*resps, args.T("api.command_invite.success", map[string]any{ + "User": targetUser.Username, + "Channel": targetChannel.Name, + })) + } } } - splitMessage := strings.SplitN(message, " ", 2) - targetUsername := splitMessage[0] - targetUsername = strings.TrimPrefix(targetUsername, "@") + if len(*resps) > 0 { + return strings.Join(*resps, "\n") + } - userProfile, nErr := a.Srv().Store().User().GetByUsername(targetUsername) - if nErr != nil { - mlog.Error(nErr.Error()) - return &model.CommandResponse{ - Text: args.T("api.command_invite.missing_user.app_error"), - ResponseType: model.CommandResponseTypeEphemeral, + return "" +} + +func (i *InviteProvider) parseMessage(a *app.App, c request.CTX, args *model.CommandArgs, resps *[]string, message string) ([]*model.User, []*model.Channel, string) { + splitMessage := strings.Split(message, " ") + + targetUsers := make([]*model.User, 0, 1) + targetChannels := make([]*model.Channel, 0) + + for j, msg := range splitMessage { + if msg == "" { + continue } + + if msg[0] == '@' || (msg[0] != '~' && j == 0) { + targetUsername := strings.TrimPrefix(msg, "@") + userProfile := i.getUserProfile(a, targetUsername) + if userProfile == nil { + *resps = append(*resps, args.T("api.command_invite.missing_user.app_error", map[string]any{ + "User": targetUsername, + })) + continue + } + targetUsers = append(targetUsers, userProfile) + } else { + targetChannelName := strings.TrimPrefix(msg, "~") + channelToJoin, err := a.GetChannelByName(c, targetChannelName, args.TeamId, false) + if err != nil { + *resps = append(*resps, args.T("api.command_invite.channel.error", map[string]any{ + "Channel": targetChannelName, + })) + continue + } + targetChannels = append(targetChannels, channelToJoin) + } + } + + if len(targetUsers) == 0 { + if len(*resps) != 0 { + return nil, nil, strings.Join(*resps, "\n") + } + return nil, nil, args.T("api.command_invite.missing_message.app_error") + } + + if len(targetChannels) == 0 { + if len(*resps) != 0 { + return nil, nil, strings.Join(*resps, "\n") + } + + channelToJoin, err := a.GetChannel(c, args.ChannelId) + if err != nil { + return nil, nil, args.T("api.command_invite.channel.app_error") + } + targetChannels = append(targetChannels, channelToJoin) + } + + return targetUsers, targetChannels, "" +} + +func (i *InviteProvider) getUserProfile(a *app.App, username string) *model.User { + userProfile, nErr := a.Srv().Store().User().GetByUsername(username) + if nErr != nil { + return nil } if userProfile.DeleteAt != 0 { - return &model.CommandResponse{ - Text: args.T("api.command_invite.missing_user.app_error"), - ResponseType: model.CommandResponseTypeEphemeral, - } + return nil } - var channelToJoin *model.Channel + return userProfile +} + +func (i *InviteProvider) checkPermissions(a *app.App, c request.CTX, args *model.CommandArgs, resps *[]string, targetUser *model.User, targetChannels []*model.Channel) []*model.Channel { var err *model.AppError - // User set a channel to add the invited user - if len(splitMessage) > 1 && splitMessage[1] != "" { - targetChannelName := strings.TrimPrefix(strings.TrimSpace(splitMessage[1]), "~") - - if channelToJoin, err = a.GetChannelByName(c, targetChannelName, args.TeamId, false); err != nil { - return &model.CommandResponse{ - Text: args.T("api.command_invite.channel.error", map[string]any{ - "Channel": targetChannelName, - }), - ResponseType: model.CommandResponseTypeEphemeral, + validChannels := make([]*model.Channel, 0, len(targetChannels)) + for _, targetChannel := range targetChannels { + switch targetChannel.Type { + case model.ChannelTypeOpen: + if !a.HasPermissionToChannel(c, args.UserId, targetChannel.Id, model.PermissionManagePublicChannelMembers) { + *resps = append(*resps, args.T("api.command_invite.permission.app_error", map[string]any{ + "User": targetUser.Username, + "Channel": targetChannel.Name, + })) + continue } - } - } else { - channelToJoin, err = a.GetChannel(c, args.ChannelId) - if err != nil { - return &model.CommandResponse{ - Text: args.T("api.command_invite.channel.app_error"), - ResponseType: model.CommandResponseTypeEphemeral, - } - } - } - - // Permissions Check - switch channelToJoin.Type { - case model.ChannelTypeOpen: - if !a.HasPermissionToChannel(c, args.UserId, channelToJoin.Id, model.PermissionManagePublicChannelMembers) { - return &model.CommandResponse{ - Text: args.T("api.command_invite.permission.app_error", map[string]any{ - "User": userProfile.Username, - "Channel": channelToJoin.Name, - }), - ResponseType: model.CommandResponseTypeEphemeral, - } - } - case model.ChannelTypePrivate: - if !a.HasPermissionToChannel(c, args.UserId, channelToJoin.Id, model.PermissionManagePrivateChannelMembers) { - if _, err = a.GetChannelMember(c, channelToJoin.Id, args.UserId); err == nil { - // User doing the inviting is a member of the channel. - return &model.CommandResponse{ - Text: args.T("api.command_invite.permission.app_error", map[string]any{ - "User": userProfile.Username, - "Channel": channelToJoin.Name, - }), - ResponseType: model.CommandResponseTypeEphemeral, + case model.ChannelTypePrivate: + if !a.HasPermissionToChannel(c, args.UserId, targetChannel.Id, model.PermissionManagePrivateChannelMembers) { + if _, err = a.GetChannelMember(c, targetChannel.Id, args.UserId); err == nil { + // User doing the inviting is a member of the channel. + *resps = append(*resps, args.T("api.command_invite.permission.app_error", map[string]any{ + "User": targetUser.Username, + "Channel": targetChannel.Name, + })) + continue } + // User doing the inviting is *not* a member of the channel. + *resps = append(*resps, args.T("api.command_invite.private_channel.app_error", map[string]any{ + "Channel": targetChannel.Name, + })) + continue } - // User doing the inviting is *not* a member of the channel. - return &model.CommandResponse{ - Text: args.T("api.command_invite.private_channel.app_error", map[string]any{ - "Channel": channelToJoin.Name, - }), - ResponseType: model.CommandResponseTypeEphemeral, - } - } - default: - return &model.CommandResponse{ - Text: args.T("api.command_invite.directchannel.app_error"), - ResponseType: model.CommandResponseTypeEphemeral, + default: + *resps = append(*resps, args.T("api.command_invite.directchannel.app_error")) + continue } + validChannels = append(validChannels, targetChannel) } + return validChannels +} +func (i *InviteProvider) addUserToChannel(a *app.App, c request.CTX, args *model.CommandArgs, userProfile *model.User, channelToJoin *model.Channel) string { // Check if user is already in the channel - _, err = a.GetChannelMember(c, channelToJoin.Id, userProfile.Id) + _, err := a.GetChannelMember(c, channelToJoin.Id, userProfile.Id) if err == nil { - return &model.CommandResponse{ - Text: args.T("api.command_invite.user_already_in_channel.app_error", map[string]any{ - "User": userProfile.Username, - }), - ResponseType: model.CommandResponseTypeEphemeral, - } + return args.T("api.command_invite.user_already_in_channel.app_error", map[string]any{ + "User": userProfile.Username, + }) } - if _, err := a.AddChannelMember(c, userProfile.Id, channelToJoin, app.ChannelMemberOpts{ - UserRequestorID: args.UserId, - }); err != nil { - var text string + if _, err = a.AddChannelMember(c, userProfile.Id, channelToJoin, app.ChannelMemberOpts{UserRequestorID: args.UserId}); err != nil { if err.Id == "api.channel.add_members.user_denied" { - text = args.T("api.command_invite.group_constrained_user_denied") + return args.T("api.command_invite.group_constrained_user_denied") } else if err.Id == "app.team.get_member.missing.app_error" || err.Id == "api.channel.add_user.to.channel.failed.deleted.app_error" { - text = args.T("api.command_invite.user_not_in_team.app_error", map[string]any{ + return args.T("api.command_invite.user_not_in_team.app_error", map[string]any{ "Username": userProfile.Username, }) - } else { - text = args.T("api.command_invite.fail.app_error") - } - return &model.CommandResponse{ - Text: text, - ResponseType: model.CommandResponseTypeEphemeral, } + return args.T("api.command_invite.fail.app_error") } - if args.ChannelId != channelToJoin.Id { - return &model.CommandResponse{ - Text: args.T("api.command_invite.success", map[string]any{ - "User": userProfile.Username, - "Channel": channelToJoin.Name, - }), - ResponseType: model.CommandResponseTypeEphemeral, - } - } - - return &model.CommandResponse{} + return "" } diff --git a/app/slashcommands/command_invite_test.go b/app/slashcommands/command_invite_test.go index 5de53899c0..5be71aaadf 100644 --- a/app/slashcommands/command_invite_test.go +++ b/app/slashcommands/command_invite_test.go @@ -17,46 +17,7 @@ func TestInviteProvider(t *testing.T) { th := setup(t).initBasic() defer th.tearDown() - channel := th.createChannel(th.BasicTeam, model.ChannelTypeOpen) - privateChannel := th.createChannel(th.BasicTeam, model.ChannelTypePrivate) - dmChannel := th.createDmChannel(th.BasicUser2) - privateChannel2 := th.createChannelWithAnotherUser(th.BasicTeam, model.ChannelTypePrivate, th.BasicUser2.Id) - - basicUser3 := th.createUser() - th.linkUserToTeam(basicUser3, th.BasicTeam) - basicUser4 := th.createUser() - deactivatedUser := th.createUser() - th.App.UpdateActive(th.Context, deactivatedUser, false) - - var err *model.AppError - _, err = th.App.CreateBot(th.Context, &model.Bot{ - Username: "bot1", - OwnerId: basicUser3.Id, - Description: "a test bot", - }) - require.Nil(t, err) - - bot2, err := th.App.CreateBot(th.Context, &model.Bot{ - Username: "bot2", - OwnerId: basicUser3.Id, - Description: "a test bot", - }) - require.Nil(t, err) - _, _, err = th.App.AddUserToTeam(th.Context, th.BasicTeam.Id, bot2.UserId, basicUser3.Id) - require.Nil(t, err) - - bot3, err := th.App.CreateBot(th.Context, &model.Bot{ - Username: "bot3", - OwnerId: basicUser3.Id, - Description: "a test bot", - }) - require.Nil(t, err) - _, _, err = th.App.AddUserToTeam(th.Context, th.BasicTeam.Id, bot3.UserId, basicUser3.Id) - require.Nil(t, err) - err = th.App.RemoveUserFromTeam(th.Context, th.BasicTeam.Id, bot3.UserId, basicUser3.Id) - require.Nil(t, err) - - InviteP := InviteProvider{} + inviteProvider := InviteProvider{} args := &model.CommandArgs{ T: func(s string, args ...any) string { return s }, ChannelId: th.BasicChannel.Id, @@ -64,115 +25,188 @@ func TestInviteProvider(t *testing.T) { UserId: th.BasicUser.Id, } - userAndWrongChannel := "@" + th.BasicUser2.Username + " wrongchannel1" - userAndChannel := "@" + th.BasicUser2.Username + " ~" + channel.Name + " " - userAndDisplayChannel := "@" + th.BasicUser2.Username + " ~" + channel.DisplayName + " " - userAndPrivateChannel := "@" + th.BasicUser2.Username + " ~" + privateChannel.Name - userAndDMChannel := "@" + basicUser3.Username + " ~" + dmChannel.Name - userAndInvalidPrivate := "@" + basicUser3.Username + " ~" + privateChannel2.Name - deactivatedUserPublicChannel := "@" + deactivatedUser.Username + " ~" + channel.Name - - groupChannel := th.createChannel(th.BasicTeam, model.ChannelTypePrivate) - _, err = th.App.AddChannelMember(th.Context, th.BasicUser.Id, groupChannel, app.ChannelMemberOpts{}) - require.Nil(t, err) - groupChannel.GroupConstrained = model.NewBool(true) - groupChannel, _ = th.App.UpdateChannel(th.Context, groupChannel) - - groupChannelNonUser := "@" + th.BasicUser2.Username + " ~" + groupChannel.Name - - tests := []struct { - desc string - expected string - msg string - }{ - { - desc: "Missing user and channel in the command", - expected: "api.command_invite.missing_message.app_error", - msg: "", - }, - { - desc: "User added in the current channel", - expected: "", - msg: th.BasicUser2.Username, - }, - { - desc: "Add user to another channel not the current", - expected: "api.command_invite.success", - msg: userAndChannel, - }, - { - desc: "try to add a user to a direct channel", - expected: "api.command_invite.directchannel.app_error", - msg: userAndDMChannel, - }, - { - desc: "Try to add a user to a invalid channel", - expected: "api.command_invite.channel.error", - msg: userAndWrongChannel, - }, - { - desc: "Try to add a user to an private channel", - expected: "api.command_invite.success", - msg: userAndPrivateChannel, - }, - { - desc: "Using display channel name which is different form Channel name", - expected: "api.command_invite.channel.error", - msg: userAndDisplayChannel, - }, - { - desc: "Invalid user to current channel", - expected: "api.command_invite.missing_user.app_error", - msg: "@invalidUser123", - }, - { - desc: "Invalid user to current channel without @", - expected: "api.command_invite.missing_user.app_error", - msg: "invalidUser321", - }, - { - desc: "try to add a user which is not part of the team", - expected: "api.command_invite.user_not_in_team.app_error", - msg: basicUser4.Username, - }, - { - desc: "try to add a user not part of the group to a group channel", - expected: "api.command_invite.group_constrained_user_denied", - msg: groupChannelNonUser, - }, - { - desc: "try to add a user to a private channel with no permission", - expected: "api.command_invite.private_channel.app_error", - msg: userAndInvalidPrivate, - }, - { - desc: "try to add a deleted user to a public channel", - expected: "api.command_invite.missing_user.app_error", - msg: deactivatedUserPublicChannel, - }, - { - desc: "try to add bot to a public channel", - expected: "api.command_invite.user_not_in_team.app_error", - msg: "@bot1", - }, - { - desc: "add bot to a public channel", - expected: "", - msg: "@bot2", - }, - { - desc: "try to add bot removed from a team to a public channel", - expected: "api.command_invite.user_not_in_team.app_error", - msg: "@bot3", - }, + runCmd := func(msg string, expected string) { + actual := inviteProvider.DoCommand(th.App, th.Context, args, msg).Text + assert.Equal(t, expected, actual) } - for _, test := range tests { - t.Run(test.desc, func(t *testing.T) { - actual := InviteP.DoCommand(th.App, th.Context, args, test.msg).Text - assert.Equal(t, test.expected, actual) - }) + checkIsMember := func(channelID, userID string) { + _, channelMemberErr := th.App.GetChannelMember(th.Context, channelID, userID) + require.Nil(t, channelMemberErr, "Failed to add user to channel") } + + checkIsNotMember := func(channelID, userID string) { + _, channelMemberErr := th.App.GetChannelMember(th.Context, channelID, userID) + require.NotNil(t, channelMemberErr, "Failed to add user to channel") + } + + t.Run("try to add missing user and channel in the command", func(t *testing.T) { + msg := "" + runCmd(msg, "api.command_invite.missing_message.app_error") + }) + + t.Run("user added in the current channel", func(t *testing.T) { + msg := th.BasicUser2.Username + runCmd(msg, "") + checkIsMember(th.BasicChannel.Id, th.BasicUser2.Id) + }) + + t.Run("add user to another channel not the current", func(t *testing.T) { + channel := th.createChannel(th.BasicTeam, model.ChannelTypeOpen) + + msg := "@" + th.BasicUser2.Username + " ~" + channel.Name + " " + runCmd(msg, "api.command_invite.success") + checkIsMember(channel.Id, th.BasicUser2.Id) + }) + + t.Run("add a user to a private channel", func(t *testing.T) { + privateChannel := th.createChannel(th.BasicTeam, model.ChannelTypePrivate) + + msg := "@" + th.BasicUser2.Username + " ~" + privateChannel.Name + runCmd(msg, "api.command_invite.success") + checkIsMember(privateChannel.Id, th.BasicUser2.Id) + }) + + t.Run("add multiple users to multiple channels", func(t *testing.T) { + anotherUser := th.createUser() + th.linkUserToTeam(anotherUser, th.BasicTeam) + channel1 := th.createChannel(th.BasicTeam, model.ChannelTypeOpen) + channel2 := th.createChannel(th.BasicTeam, model.ChannelTypeOpen) + + msg := "@" + th.BasicUser2.Username + " @" + anotherUser.Username + " ~" + channel1.Name + " ~" + channel2.Name + expected := "api.command_invite.success\napi.command_invite.success\napi.command_invite.success\napi.command_invite.success" + runCmd(msg, expected) + checkIsMember(channel1.Id, th.BasicUser2.Id) + checkIsMember(channel2.Id, th.BasicUser2.Id) + checkIsMember(channel1.Id, anotherUser.Id) + checkIsMember(channel2.Id, anotherUser.Id) + }) + + t.Run("adds multiple users even when some are invalid or already members", func(t *testing.T) { + channel := th.createChannel(th.BasicTeam, model.ChannelTypeOpen) + userAlreadyInChannel := th.createUser() + th.linkUserToTeam(userAlreadyInChannel, th.BasicTeam) + th.addUserToChannel(userAlreadyInChannel, channel) + userInTeam := th.createUser() + th.linkUserToTeam(userInTeam, th.BasicTeam) + userNotInTeam := th.createUser() + + msg := "@invalidUser123 @" + userAlreadyInChannel.Username + " @" + userInTeam.Username + " @" + userNotInTeam.Username + " ~" + channel.Name + expected := "api.command_invite.missing_user.app_error\n" + expected += "api.command_invite.user_already_in_channel.app_error\n" + expected += "api.command_invite.success\n" + expected += "api.command_invite.user_not_in_team.app_error" + runCmd(msg, expected) + checkIsMember(channel.Id, userInTeam.Id) + }) + + t.Run("try to add a user to a direct channel", func(t *testing.T) { + anotherUser := th.createUser() + th.linkUserToTeam(anotherUser, th.BasicTeam) + directChannel := th.createDmChannel(th.BasicUser2) + + msg := "@" + anotherUser.Username + " ~" + directChannel.Name + runCmd(msg, "api.command_invite.directchannel.app_error") + checkIsNotMember(directChannel.Id, anotherUser.Id) + }) + + t.Run("try to add a user to an invalid channel", func(t *testing.T) { + msg := "@" + th.BasicUser2.Username + " wrongchannel1" + runCmd(msg, "api.command_invite.channel.error") + }) + + t.Run("try to add a user using channel's display name", func(t *testing.T) { + channel := th.createChannel(th.BasicTeam, model.ChannelTypeOpen) + + msg := "@" + th.BasicUser2.Username + " ~" + channel.DisplayName + runCmd(msg, "api.command_invite.channel.error") + checkIsNotMember(channel.Id, th.BasicUser2.Id) + }) + + t.Run("try add invalid user to current channel", func(t *testing.T) { + msg := "@invalidUser123" + runCmd(msg, "api.command_invite.missing_user.app_error") + }) + + t.Run("invalid user to current channel without @", func(t *testing.T) { + msg := "invalidUser123" + runCmd(msg, "api.command_invite.missing_user.app_error") + }) + + t.Run("try to add a user which is not part of the team", func(t *testing.T) { + anotherUser := th.createUser() + // Do not add user to the team + + msg := anotherUser.Username + runCmd(msg, "api.command_invite.user_not_in_team.app_error") + }) + + t.Run("try to add a user not part of the group to a group channel", func(t *testing.T) { + groupChannel := th.createChannel(th.BasicTeam, model.ChannelTypePrivate) + _, err := th.App.AddChannelMember(th.Context, th.BasicUser.Id, groupChannel, app.ChannelMemberOpts{}) + require.Nil(t, err) + groupChannel.GroupConstrained = model.NewBool(true) + groupChannel, _ = th.App.UpdateChannel(th.Context, groupChannel) + + msg := "@" + th.BasicUser2.Username + " ~" + groupChannel.Name + runCmd(msg, "api.command_invite.group_constrained_user_denied") + checkIsNotMember(groupChannel.Id, th.BasicUser2.Id) + }) + + t.Run("try to add a user to a private channel with no permission", func(t *testing.T) { + anotherUser := th.createUser() + th.linkUserToTeam(anotherUser, th.BasicTeam) + privateChannel := th.createChannelWithAnotherUser(th.BasicTeam, model.ChannelTypePrivate, th.BasicUser2.Id) + + msg := "@" + anotherUser.Username + " ~" + privateChannel.Name + runCmd(msg, "api.command_invite.private_channel.app_error") + checkIsNotMember(privateChannel.Id, anotherUser.Id) + }) + + t.Run("try to add a deleted user to a public channel", func(t *testing.T) { + channel := th.createChannel(th.BasicTeam, model.ChannelTypeOpen) + deactivatedUser := th.createUser() + _, appErr := th.App.UpdateActive(th.Context, deactivatedUser, false) + require.Nil(t, appErr) + + msg := "@" + deactivatedUser.Username + " ~" + channel.Name + runCmd(msg, "api.command_invite.missing_user.app_error") + checkIsNotMember(channel.Id, deactivatedUser.Id) + }) + + t.Run("add bot to a public channel", func(t *testing.T) { + bot, appErr := th.App.CreateBot(th.Context, &model.Bot{Username: "bot_" + model.NewId(), OwnerId: th.BasicUser2.Id}) + require.Nil(t, appErr) + _, _, appErr = th.App.AddUserToTeam(th.Context, th.BasicTeam.Id, bot.UserId, th.BasicUser2.Id) + require.Nil(t, appErr) + + msg := "@" + bot.Username + runCmd(msg, "") + checkIsMember(th.BasicChannel.Id, bot.UserId) + }) + + t.Run("try to add bot to a public channel without being a member", func(t *testing.T) { + bot, appErr := th.App.CreateBot(th.Context, &model.Bot{Username: "bot_" + model.NewId(), OwnerId: th.BasicUser2.Id}) + require.Nil(t, appErr) + // Do not add to the team + + msg := "@" + bot.Username + runCmd(msg, "api.command_invite.user_not_in_team.app_error") + checkIsNotMember(th.BasicChannel.Id, bot.UserId) + }) + + t.Run("try to add bot removed from a team to a public channel", func(t *testing.T) { + bot, appErr := th.App.CreateBot(th.Context, &model.Bot{Username: "bot_" + model.NewId(), OwnerId: th.BasicUser2.Id}) + require.Nil(t, appErr) + _, _, appErr = th.App.AddUserToTeam(th.Context, th.BasicTeam.Id, bot.UserId, th.BasicUser2.Id) + require.Nil(t, appErr) + appErr = th.App.RemoveUserFromTeam(th.Context, th.BasicTeam.Id, bot.UserId, th.BasicUser2.Id) + require.Nil(t, appErr) + + msg := "@" + bot.Username + runCmd(msg, "api.command_invite.user_not_in_team.app_error") + checkIsNotMember(th.BasicChannel.Id, bot.UserId) + }) } func TestInviteGroup(t *testing.T) { diff --git a/i18n/en.json b/i18n/en.json index 4cb1c9c634..0fac9e2df3 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -900,15 +900,15 @@ }, { "id": "api.command_invite.hint", - "translation": "@[username] ~[channel]" + "translation": "@[username]... ~[channel]..." }, { "id": "api.command_invite.missing_message.app_error", - "translation": "Missing Username and Channel." + "translation": "Missing Username and/or Channel." }, { "id": "api.command_invite.missing_user.app_error", - "translation": "We couldn't find the user. They may have been deactivated by the System Administrator." + "translation": "We couldn't find the user {{.User}}. They may have been deactivated by the System Administrator." }, { "id": "api.command_invite.name", @@ -920,7 +920,7 @@ }, { "id": "api.command_invite.private_channel.app_error", - "translation": "Could not find the channel {{.Channel}}. Please use the channel handle to identify channels." + "translation": "Could not find the channel {{.Channel}}. Please use the [channel handle](https://docs.mattermost.com/messaging/managing-channels.html#naming-a-channel) to identify channels." }, { "id": "api.command_invite.success",