From ed23df6a2e48a4b229e8fb37c28104304b31b91d Mon Sep 17 00:00:00 2001 From: Mylon Suren <23694620+mylonsuren@users.noreply.github.com> Date: Tue, 3 Jan 2023 16:30:30 -0500 Subject: [PATCH 1/9] [MM-47489] Move Shared Channels (Experimental) to Professional (#21882) * Move shared channels to professional license * Remove references to SharedChannels from license and use license SKU instead * add tests * Refactor shared channels license check and add tests * Re-add removed negation on license check Co-authored-by: Mattermod --- app/server.go | 2 +- config/client.go | 2 +- config/client_test.go | 72 +++++++++++++++++++++++++++++++++++++++++++ model/license.go | 10 ++++++ model/license_test.go | 50 ++++++++++++++++++++++++++++++ 5 files changed, 134 insertions(+), 2 deletions(-) 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/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/model/license.go b/model/license.go index 94f0b81da4..cf5c30a258 100644 --- a/model/license.go +++ b/model/license.go @@ -312,6 +312,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()) + }) + } +} From 254bc4f3a36771f9a1381356cfb1595aa8a00c85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Csaba=20T=C3=B3th=20//=20BDSC=20Business=20Digitalisation?= =?UTF-8?q?=20Kft?= Date: Tue, 3 Jan 2023 00:07:47 +0100 Subject: [PATCH 2/9] Translated using Weblate (Hungarian) Currently translated at 96.0% (2333 of 2428 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/hu/ --- i18n/hu.json | 4 ++++ 1 file changed, 4 insertions(+) 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." } ] From b61545804f9de03b6f7e41962d3c77e910f0fe7d Mon Sep 17 00:00:00 2001 From: Ji-Hyeon Gim Date: Tue, 3 Jan 2023 00:07:47 +0100 Subject: [PATCH 3/9] Translated using Weblate (Korean) Currently translated at 82.7% (2008 of 2428 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/ko/ --- i18n/ko.json | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) 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": "구성원을 그룹-팀 및 그룹-채널에 추가하지 못했습니다" } ] From d4de6e6120bebe57c4b8805d3a53d156d7a6df0a Mon Sep 17 00:00:00 2001 From: Konstantinos Pittas Date: Wed, 4 Jan 2023 21:49:15 +0200 Subject: [PATCH 4/9] [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", From 6b41f914cc21d6fed6f201ea327457f2b77572e7 Mon Sep 17 00:00:00 2001 From: Ibrahim Serdar Acikgoz Date: Thu, 5 Jan 2023 10:00:28 +0300 Subject: [PATCH 5/9] cicleci: add ability use target branch for pulling focalboard (#21990) --- .circleci/config.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index e452a6a2f4..e10fa93097 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 @@ -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: From 1078292ac6d99b9fda4b6c518d59979b1ba70954 Mon Sep 17 00:00:00 2001 From: Agniva De Sarker Date: Thu, 5 Jan 2023 19:33:14 +0530 Subject: [PATCH 6/9] MM-45967: Print diff output in go mod tidy (#21984) https://mattermost.atlassian.net/browse/MM-45967 ```release-note NONE ``` --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index e10fa93097..c53ed45a72 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -140,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 From a83170e753148d01612f7141455ec20461fbf595 Mon Sep 17 00:00:00 2001 From: Agniva De Sarker Date: Thu, 5 Jan 2023 19:34:22 +0530 Subject: [PATCH 7/9] MM-49395: Increase compression for previews and emojis (#21985) We also reduce the max size of uploaded emojis to 512KiB. https://mattermost.atlassian.net/browse/MM-49395 ```release-note Max size of uploaded emojis is now reduced to 512KiB to reduce image download bandwidth. ``` --- app/emoji.go | 7 +++---- app/imaging/encode.go | 4 +++- app/user_test.go | 2 +- app/users/profile_picture.go | 5 ++++- i18n/en.json | 2 +- tests/10000x1_expected_preview.png | Bin 92 -> 92 bytes tests/10000x1_expected_thumb.png | Bin 76 -> 76 bytes tests/1x10000_expected_preview.png | Bin 121 -> 121 bytes tests/1x10000_expected_thumb.png | Bin 79 -> 79 bytes tests/fill_test_16bit_rgb_out.png | Bin 1218 -> 1011 bytes tests/fill_test_16bit_rgba_out.png | Bin 1568 -> 1338 bytes tests/fill_test_8bit_palette_out.png | Bin 140 -> 140 bytes tests/fill_test_8bit_rgb_out.png | Bin 896 -> 700 bytes tests/fill_test_8bit_rgba_out.png | Bin 1117 -> 897 bytes 14 files changed, 12 insertions(+), 8 deletions(-) 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/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/i18n/en.json b/i18n/en.json index 0fac9e2df3..c9d23f4a46 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -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", diff --git a/tests/10000x1_expected_preview.png b/tests/10000x1_expected_preview.png index bf2ca9cc218dc3f05531b743122fc2ae61df969e..6c48a5242149680b56b3660e0dc6402c64cf7835 100644 GIT binary patch delta 26 gcma!vnV`jZYofNIfTQ@H4hA6bboFyt=akR{0BS!6$p8QV delta 26 gcma!vnV`iuXQH;E0M8+t4Gciw>FVdQ&MBb@0A}U~sQ>@~ diff --git a/tests/10000x1_expected_thumb.png b/tests/10000x1_expected_thumb.png index a354c410473a0afaaffa29debb380348e798873c..93a8cb36673e6f0902388694de551d25bd174d73 100644 GIT binary patch delta 39 ucmebAnV_X~E9vKX1Fjd042%p6|NrN1VPrY+R!Wxv2s~Z=T-G@yGywo3!ws(h delta 39 ucmebAnV_XKC+X*T1Fjd042%p6|NrN1VPvV1xpsm92s~Z=T-G@yGywn{uMMjJ diff --git a/tests/1x10000_expected_preview.png b/tests/1x10000_expected_preview.png index b4317244e90f3076aac8255efdbd9b723e7bfc97..ba938e6aefeda805e2b8a648ad833854529b8763 100644 GIT binary patch delta 26 gcmb=doM6LvYoe`=fSicZYz83kboFyt=akR{0BP^mW5Ul_J delta 42 xcmebGpP;2WC+XMu0|$;6bg^@@u`n?F|F83&@x;#w8jBc!z|+;wWt~$(699U?5QYE% diff --git a/tests/fill_test_16bit_rgb_out.png b/tests/fill_test_16bit_rgb_out.png index bb9741d53fe3a0276abce816ce948732c6a00577..72f7b9ca99ae6326e8cd3655aeafd9cb54345a33 100644 GIT binary patch literal 1011 zcmeAS@N?(olHy`uVBq!ia0y~yV5|bN{|GPv$z^9$zA`W{@A7nU45_&F=Eg?B1_Kf2 zjrn@N*XLAmtq@)!7_;~DtW0)>2LjVM85jzg85Hsu7>@8VH0)r6Q3B=+43F3t9PYrW z4r#bjR2@L2AdN^mfJ#9ck#qnJN3sK|lu{iH=BHWeFQ|nve4D-{>_PW)Hj3Q<3JX+M zlNTh=utf48G%S$}r%51UdVo-{W9lH!%Rs-Pr2^V`*?(r{Sq6qVhlgTe~DWM4fiyuk8 diff --git a/tests/fill_test_16bit_rgba_out.png b/tests/fill_test_16bit_rgba_out.png index 491bee1ce511e72a546b06d0a76c401a4940c297..7e521ccbe65e8452ff891a6ad25e1ed8b6023ff2 100644 GIT binary patch literal 1338 zcmeAS@N?(olHy`uVBq!ia0y~yV5|bN{|K-FNzHYe7cnrfGJ3i=hE&{ob8BPnAqSC$ zht{XB@EqjOaX9L;=AcT?5jK+xjCF!lJ3J;^^oi&_`mOS1LG9VUc6X)=O_XO~c(DKF z8lX`uwhRmkFBlmZ9Qhd-n&cQ51immZG%R3;$Z!Ddbhy9_kwH-kl0i`lG6b#^Bm*}U zW(eF=xD1MYAP2ydf?a}QABs|t42n_=`%s)_;F!<4MPt#mpk2?IpFg%{IAiInb`<+i;sGp!>@?~IE{)@z z#Gru31uQ)A2L(!~(JTw>4_YnD@Ze9w`mbRQ)k;P4&RgnFpR;}TU4{>P6Hip`y~UdU zkz@8p6E=}OR!_5amsbBP-V*gRM$f+8j=`tU=Grd{Yd>{hVg+VH-%ot#Ngw7>uQ zhq0%Rh&XbZIJo$3aa5h+!mcub@!n)pi(TG)oc;fIzn{D&{q)Jx-@aFTJH09QF)IVZ zl{abnK+|gGFJNbAFmU8&U`T9|V_-PMV#~nL!U?pFL--2=gMiu#Mg|3+3(O1-Ge(BO z+vnpL{-xZ$RvK9rZ*S56p7BAx?0mJ`+jQCwzwv#b&RL~*isR?%)gNCzOgwY@wxPZK z^ISHEb@znVu`W*y*r*^3UD74d1_CzK+@9%H_*%cpu2zS@^ukaw7W!YrpUu zKlQI4@<(Rij{17=W~7)6rE~W(GPur@{}cahvv|Y(+}ktOuTS2$xA*5q!+doHE53Ha z`}aXFi!#-|vZ>!+2|=t-XNI7UkItK;Y@>=d#Wzp$Pzal?YV; delta 28 icmeBS>|vZ>!#HQ6t-ZjcBN7)FfWXt$&t;ucLK6Ugh6vFB diff --git a/tests/fill_test_8bit_rgb_out.png b/tests/fill_test_8bit_rgb_out.png index 499026d665ee0a33dac235b00193272a5cbc962b..3214f84c0fa4896dc9018ef3458faf4a8f483664 100644 GIT binary patch literal 700 zcmeAS@N?(olHy`uVBq!ia0y~yV5|bN|8Ot?$!{?~wlOd;HG8@^hE&{obL}86gCd7Z z;EfCQr#I_X9PEBy`e4Q%gQ-kA9vCw!m^a*EIKd027>bxdJTL{60r9{TSV0jpTpd&w z19PdQgMd|=C%wKt#=p%cQPs!F}plr>o~zHFi~1T$=tyuuc2em zNX&W3$oy_z+yUlqbK^eP-W!x|9GoFHpsspTx?G(h|HEKPNhF5HKSlB0S728<0Cm=(S- zaM&_9@xz!f8ov-w7F{FJLO>f(Y=qbZ7CNv_#%IG^LtXLbDV#r^&FD!p=J*?@TpxKi z-27_0YeKEhDt4gAyxP6b@>g32AMor~v+$pDqi?#+95ea&uSljM`~&v~aY7un0~!FR zmLquxiAi0LpxOwFR2Yqr{eN0bxZ(51>%a(_8MrrO{ng!;390Muohj&(F4Jjzw0!FP zY3moie)USCHsNiilz#aaYZ5>(YPJ6t@BHYB!Np1N{ZQ~U7Cs^67LKg>R#yZ=1%+8RcM*GJzmGW>~l z;ulc=!caK$sP9{<9Zp)ktDkfHooqqU>xkmpwvVz}{yAPV)K7o*_spJ|Gv#-GKft+m z=Fbh)M?^XQKK`@o+5K~8cHeb=aK!GNT!HJo7xJf7`RmK7_r3agzC}*K?*-$dQEI3L z!M}}tcbOj?`D15RdCOKIt4(#NSb@pa?R zuPlEZcyA}0ZTDWT&=i>ZpZ5;O6!0z`SW^7||L~f;o9i|U86bzj)78&qol`;+0Dc3) AW&i*H From 8c7863f9eebc2c89cfd87b60f283816e19818251 Mon Sep 17 00:00:00 2001 From: Allan Guwatudde Date: Thu, 5 Jan 2023 17:10:31 +0300 Subject: [PATCH 8/9] [MM-49344] - Improve error message when trying to upload another trial license (#21969) --- i18n/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/i18n/en.json b/i18n/en.json index c9d23f4a46..b0cf93cf7e 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -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", From 8685101d2045eee875e7538eed811f2246d9eb73 Mon Sep 17 00:00:00 2001 From: Christopher Speller Date: Thu, 5 Jan 2023 09:50:30 -0800 Subject: [PATCH 9/9] Pre-pacakge Playbooks v1.35.0 (#21991) --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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