diff --git a/.circleci/config.yml b/.circleci/config.yml index e452a6a2f4..c53ed45a72 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -21,6 +21,10 @@ executors: jobs: setup-multi-product-repositories: + parameters: + target-branch: + type: string + default: '$(curl -s -H "Authorization: token ${GITHUB_TOKEN}" $(echo https://api.github.com/repos/${CIRCLE_PULL_REQUEST:19} | sed "s/\/pull\//\/pulls\//") | jq ".base.ref" | tr -d "\042" )' working_directory: /mnt/ramdisk/mattermost-server docker: - image: cimg/go:1.18 @@ -31,7 +35,7 @@ jobs: cd .. git clone --depth=1 --no-single-branch https://github.com/mattermost/focalboard.git cd focalboard - git checkout $CIRCLE_BRANCH || git checkout rolling-stable + git checkout $CIRCLE_BRANCH || git checkout <> || git checkout rolling-stable echo $(git rev-parse HEAD) cd ../mattermost-server make setup-go-work @@ -136,7 +140,7 @@ jobs: command: | cd mattermost-server make modules-tidy - if [[ -n $(git status --porcelain) ]]; then echo "Please tidy up the Go modules using make modules-tidy"; exit 1; fi + if [[ -n $(git status --porcelain) ]]; then echo "Please tidy up the Go modules using make modules-tidy"; git diff; exit 1; fi check-store-layers: docker: - image: cimg/go:1.18 @@ -487,6 +491,8 @@ workflows: untagged-build: jobs: - setup-multi-product-repositories: + context: + - matterbuild-github-token filters: branches: ignore: @@ -652,6 +658,8 @@ workflows: release-build: jobs: - setup-multi-product-repositories: + context: + - matterbuild-github-token filters: branches: only: diff --git a/Makefile b/Makefile index ede4f447d0..7c8de4311b 100644 --- a/Makefile +++ b/Makefile @@ -155,7 +155,7 @@ PLUGIN_PACKAGES += mattermost-plugin-confluence-v1.3.0 PLUGIN_PACKAGES += mattermost-plugin-custom-attributes-v1.3.1 PLUGIN_PACKAGES += mattermost-plugin-github-v2.1.4 PLUGIN_PACKAGES += mattermost-plugin-gitlab-v1.5.2 -PLUGIN_PACKAGES += mattermost-plugin-playbooks-v1.34.0 +PLUGIN_PACKAGES += mattermost-plugin-playbooks-v1.35.0 PLUGIN_PACKAGES += mattermost-plugin-jenkins-v1.1.0 PLUGIN_PACKAGES += mattermost-plugin-jira-v3.2.2 PLUGIN_PACKAGES += mattermost-plugin-jitsi-v2.0.1 diff --git a/app/emoji.go b/app/emoji.go index 48e7773b5a..9982428711 100644 --- a/app/emoji.go +++ b/app/emoji.go @@ -14,7 +14,6 @@ import ( "image/draw" "image/gif" _ "image/jpeg" - "image/png" "io" "mime/multipart" "net/http" @@ -31,7 +30,7 @@ import ( ) const ( - MaxEmojiFileSize = 1 << 20 // 1 MB + MaxEmojiFileSize = 1 << 19 // 512 KiB MaxEmojiWidth = 128 MaxEmojiHeight = 128 MaxEmojiOriginalWidth = 1028 @@ -155,8 +154,8 @@ func (a *App) UploadEmojiImage(c request.CTX, id string, imageData *multipart.Fi return model.NewAppError("uploadEmojiImage", "api.emoji.upload.large_image.decode_error", nil, "", http.StatusBadRequest).Wrap(err) } - resized_image := resizeEmoji(img, config.Width, config.Height) - if err := png.Encode(newbuf, resized_image); err != nil { + resizedImg := resizeEmoji(img, config.Width, config.Height) + if err := a.ch.imgEncoder.EncodePNG(newbuf, resizedImg); err != nil { return model.NewAppError("uploadEmojiImage", "api.emoji.upload.large_image.encode_error", nil, "", http.StatusBadRequest).Wrap(err) } buf = newbuf diff --git a/app/imaging/encode.go b/app/imaging/encode.go index 1afb3159d9..d10f73c91a 100644 --- a/app/imaging/encode.go +++ b/app/imaging/encode.go @@ -45,7 +45,9 @@ func NewEncoder(opts EncoderOptions) (*Encoder, error) { e.sem = make(chan struct{}, opts.ConcurrencyLevel) } e.opts = opts - e.pngEncoder = &png.Encoder{} + e.pngEncoder = &png.Encoder{ + CompressionLevel: png.BestCompression, + } return &e, nil } diff --git a/app/server.go b/app/server.go index dc66353ee4..f7a9e9b912 100644 --- a/app/server.go +++ b/app/server.go @@ -595,7 +595,7 @@ func (s *Server) startInterClusterServices(license *model.License) error { // Shared Channels service // License check - if !*license.Features.SharedChannels { + if !license.HasSharedChannels() { mlog.Debug("License does not have shared channels enabled") return nil } 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/app/user_test.go b/app/user_test.go index 086e71e25f..b4a57dd32a 100644 --- a/app/user_test.go +++ b/app/user_test.go @@ -121,7 +121,7 @@ func TestAdjustProfileImage(t *testing.T) { assert.True(t, adjusted.Len() > 0) assert.NotEqual(t, testjpg, adjusted) - // default image should require adjustment + // default image should not require adjustment user := th.BasicUser image, err := th.App.GetDefaultProfileImage(user) require.Nil(t, err) diff --git a/app/users/profile_picture.go b/app/users/profile_picture.go index 1858140e52..4a9c73f36f 100644 --- a/app/users/profile_picture.go +++ b/app/users/profile_picture.go @@ -160,7 +160,10 @@ func createProfileImage(username string, userID string, initialFont string) ([]b buf := new(bytes.Buffer) - if imgErr := png.Encode(buf, dstImg); imgErr != nil { + enc := png.Encoder{ + CompressionLevel: png.BestCompression, + } + if imgErr := enc.Encode(buf, dstImg); imgErr != nil { return nil, ImageEncodingError } diff --git a/config/client.go b/config/client.go index e26c2bea40..6ce5433dc1 100644 --- a/config/client.go +++ b/config/client.go @@ -196,7 +196,7 @@ func GenerateClientConfig(c *model.Config, telemetryID string, license *model.Li props["DataRetentionBoardsRetentionDays"] = strconv.FormatInt(int64(*c.DataRetentionSettings.BoardsRetentionDays), 10) } - if *license.Features.SharedChannels { + if license.HasSharedChannels() { props["ExperimentalSharedChannels"] = strconv.FormatBool(*c.ExperimentalSettings.EnableSharedChannels) props["ExperimentalRemoteClusterService"] = strconv.FormatBool(c.FeatureFlags.EnableRemoteClusterService && *c.ExperimentalSettings.EnableRemoteClusterService) } diff --git a/config/client_test.go b/config/client_test.go index a13b9cea77..9c92cfe329 100644 --- a/config/client_test.go +++ b/config/client_test.go @@ -254,6 +254,78 @@ func TestGetClientConfig(t *testing.T) { "EnableCustomGroups": "false", }, }, + { + "Shared channels other license", + &model.Config{ + ExperimentalSettings: model.ExperimentalSettings{ + EnableSharedChannels: model.NewBool(true), + }, + }, + "", + &model.License{ + Features: &model.Features{ + SharedChannels: model.NewBool(false), + }, + SkuShortName: "other", + }, + map[string]string{ + "ExperimentalSharedChannels": "false", + }, + }, + { + "licensed for shared channels", + &model.Config{ + ExperimentalSettings: model.ExperimentalSettings{ + EnableSharedChannels: model.NewBool(true), + }, + }, + "", + &model.License{ + Features: &model.Features{ + SharedChannels: model.NewBool(true), + }, + SkuShortName: "other", + }, + map[string]string{ + "ExperimentalSharedChannels": "true", + }, + }, + { + "Shared channels professional license", + &model.Config{ + ExperimentalSettings: model.ExperimentalSettings{ + EnableSharedChannels: model.NewBool(true), + }, + }, + "", + &model.License{ + Features: &model.Features{ + SharedChannels: model.NewBool(false), + }, + SkuShortName: model.LicenseShortSkuProfessional, + }, + map[string]string{ + "ExperimentalSharedChannels": "true", + }, + }, + { + "Shared channels enterprise license", + &model.Config{ + ExperimentalSettings: model.ExperimentalSettings{ + EnableSharedChannels: model.NewBool(true), + }, + }, + "", + &model.License{ + Features: &model.Features{ + SharedChannels: model.NewBool(false), + }, + SkuShortName: model.LicenseShortSkuEnterprise, + }, + map[string]string{ + "ExperimentalSharedChannels": "true", + }, + }, } for _, testCase := range testCases { diff --git a/i18n/en.json b/i18n/en.json index bf696f48a5..18a5dc0dab 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", @@ -1719,7 +1719,7 @@ }, { "id": "api.emoji.create.too_large.app_error", - "translation": "Unable to create emoji. Image must be less than 1 MB in size." + "translation": "Unable to create emoji. Image must be less than 512 KiB in size." }, { "id": "api.emoji.disabled.app_error", @@ -2055,7 +2055,7 @@ }, { "id": "api.license.request-trial.can-start-trial.not-allowed", - "translation": "This trial license key for Mattermost Enterprise Edition has expired and is no longer valid. If you would like to extend your trial period please [contact our sales team](https://mattermost.com/contact-us/)." + "translation": "Failed to apply new trial license. You have previously applied a trial license to this Mattermost instance.. If you would like to extend your trial period please [contact our sales team](https://mattermost.com/contact-us/)." }, { "id": "api.license.request_renewal_link.app_error", diff --git a/i18n/hu.json b/i18n/hu.json index c010bb1704..dc185e734b 100644 --- a/i18n/hu.json +++ b/i18n/hu.json @@ -9362,5 +9362,9 @@ { "id": "model.group.name.reserved_name.app_error", "translation": "csoport név létezik mint lefoglalt név" + }, + { + "id": "api.acknowledgement.delete.archived_channel.app_error", + "translation": "Nem lehet eltávolítani egy archivált csatornában lévő jóváhagyást." } ] diff --git a/i18n/ko.json b/i18n/ko.json index ad4733a1aa..69ea350a1b 100644 --- a/i18n/ko.json +++ b/i18n/ko.json @@ -1369,11 +1369,11 @@ }, { "id": "api.post.send_notification_and_forget.push_comment_on_post", - "translation": " 당신의 게시글에 답글이 달렸습니다." + "translation": " 당신의 게시글에 댓글이 달렸습니다." }, { "id": "api.post.send_notification_and_forget.push_comment_on_thread", - "translation": " 당신이 참여한 글타래에 답글이 달렸습니다." + "translation": " 당신이 참여한 글타래에 댓글이 달렸습니다." }, { "id": "api.post.send_notifications_and_forget.push_explicit_mention", @@ -4565,7 +4565,7 @@ }, { "id": "oauth.gitlab.tos.error", - "translation": "GitLab's Terms of Service have updated. Please go to gitlab.com to accept them and then try logging into Mattermost again." + "translation": "GitLab 서비스 약관이 업데이트되었습니다. {{.URL}}으로 이동하여 수락한 다음 Mattermost에 다시 로그인해주세요." }, { "id": "plugin.api.update_user_status.bad_status", @@ -4741,7 +4741,7 @@ }, { "id": "web.error.unsupported_browser.min_os_version.mac", - "translation": "macOS 10.14 이상" + "translation": "macOS 11 버전 이상" }, { "id": "web.error.unsupported_browser.min_browser_version.safari", @@ -4753,11 +4753,11 @@ }, { "id": "web.error.unsupported_browser.min_browser_version.edge", - "translation": "44버전 이상" + "translation": "95 버전 이상" }, { "id": "web.error.unsupported_browser.min_browser_version.chrome", - "translation": "100 버전 이상" + "translation": "106 버전 이상" }, { "id": "web.error.unsupported_browser.learn_more", @@ -7241,11 +7241,11 @@ }, { "id": "api.post.send_notification_and_forget.push_comment_on_crt_thread", - "translation": " 지켜보는 중인 글타래에 답글을 남겼습니다." + "translation": " 지켜보는 글타래에 댓글을 남겼습니다." }, { "id": "api.post.send_notification_and_forget.push_comment_on_crt_thread_dm", - "translation": " 글타래에 답글을 남겼습니다." + "translation": " 글타래에 댓글을 남겼습니다." }, { "id": "api.command_remote.missing_command", @@ -8034,5 +8034,9 @@ { "id": "app.user.update_threads_read_for_user.app_error", "translation": "모든 사용자 글타래들을 읽음 상태로 변경할 수 없습니다" + }, + { + "id": "api.admin.syncables_error", + "translation": "구성원을 그룹-팀 및 그룹-채널에 추가하지 못했습니다" } ] diff --git a/model/license.go b/model/license.go index b0cb95c809..7c875dad32 100644 --- a/model/license.go +++ b/model/license.go @@ -321,6 +321,16 @@ func (l *License) HasEnterpriseMarketplacePlugins() bool { l.SkuShortName == LicenseShortSkuEnterprise } +func (l *License) HasSharedChannels() bool { + if l == nil { + return false + } + + return (l.Features != nil && l.Features.SharedChannels != nil && *l.Features.SharedChannels) || + l.SkuShortName == LicenseShortSkuProfessional || + l.SkuShortName == LicenseShortSkuEnterprise +} + // NewTestLicense returns a license that expires in the future and has the given features. func NewTestLicense(features ...string) *License { ret := &License{ diff --git a/model/license_test.go b/model/license_test.go index 6319ccc8e9..1d1a5f1acf 100644 --- a/model/license_test.go +++ b/model/license_test.go @@ -343,3 +343,53 @@ func TestLicense_IsSanctionedTrial(t *testing.T) { assert.True(t, license.IsSanctionedTrial()) }) } + +func TestLicenseHasSharedChannels(t *testing.T) { + + testCases := []struct { + description string + license License + expectedValue bool + }{ + { + "licensed for shared channels", + License{ + Features: &Features{ + SharedChannels: NewBool(true), + }, + SkuShortName: "other", + }, + true, + }, + { + "not licensed for shared channels", + License{ + Features: &Features{}, + SkuShortName: "other", + }, + false, + }, + { + "professional license for shared channels", + License{ + Features: &Features{}, + SkuShortName: LicenseShortSkuProfessional, + }, + true, + }, + { + "enterprise license for shared channels", + License{ + Features: &Features{}, + SkuShortName: LicenseShortSkuEnterprise, + }, + true, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.description, func(t *testing.T) { + assert.Equal(t, testCase.expectedValue, testCase.license.HasSharedChannels()) + }) + } +} diff --git a/tests/10000x1_expected_preview.png b/tests/10000x1_expected_preview.png index bf2ca9cc21..6c48a52421 100644 Binary files a/tests/10000x1_expected_preview.png and b/tests/10000x1_expected_preview.png differ diff --git a/tests/10000x1_expected_thumb.png b/tests/10000x1_expected_thumb.png index a354c41047..93a8cb3667 100644 Binary files a/tests/10000x1_expected_thumb.png and b/tests/10000x1_expected_thumb.png differ diff --git a/tests/1x10000_expected_preview.png b/tests/1x10000_expected_preview.png index b4317244e9..ba938e6aef 100644 Binary files a/tests/1x10000_expected_preview.png and b/tests/1x10000_expected_preview.png differ diff --git a/tests/1x10000_expected_thumb.png b/tests/1x10000_expected_thumb.png index 71dd5eba8f..4078828348 100644 Binary files a/tests/1x10000_expected_thumb.png and b/tests/1x10000_expected_thumb.png differ diff --git a/tests/fill_test_16bit_rgb_out.png b/tests/fill_test_16bit_rgb_out.png index bb9741d53f..72f7b9ca99 100644 Binary files a/tests/fill_test_16bit_rgb_out.png and b/tests/fill_test_16bit_rgb_out.png differ diff --git a/tests/fill_test_16bit_rgba_out.png b/tests/fill_test_16bit_rgba_out.png index 491bee1ce5..7e521ccbe6 100644 Binary files a/tests/fill_test_16bit_rgba_out.png and b/tests/fill_test_16bit_rgba_out.png differ diff --git a/tests/fill_test_8bit_palette_out.png b/tests/fill_test_8bit_palette_out.png index d865b78def..5feebf44cd 100644 Binary files a/tests/fill_test_8bit_palette_out.png and b/tests/fill_test_8bit_palette_out.png differ diff --git a/tests/fill_test_8bit_rgb_out.png b/tests/fill_test_8bit_rgb_out.png index 499026d665..3214f84c0f 100644 Binary files a/tests/fill_test_8bit_rgb_out.png and b/tests/fill_test_8bit_rgb_out.png differ diff --git a/tests/fill_test_8bit_rgba_out.png b/tests/fill_test_8bit_rgba_out.png index bd5c157f2a..3167887d9d 100644 Binary files a/tests/fill_test_8bit_rgba_out.png and b/tests/fill_test_8bit_rgba_out.png differ