From 3a370e18182e1cdc124f93d70dea50d372a50d67 Mon Sep 17 00:00:00 2001 From: Caleb Roseland Date: Fri, 16 Sep 2022 15:35:02 -0500 Subject: [PATCH 01/63] add PeopleProduct flag --- model/feature_flags.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/model/feature_flags.go b/model/feature_flags.go index 209e7a9f99..3a256749cb 100644 --- a/model/feature_flags.go +++ b/model/feature_flags.go @@ -75,6 +75,8 @@ type FeatureFlags struct { PlanUpgradeButtonText string PostPriority bool + + PeopleProduct bool } func (f *FeatureFlags) SetDefaults() { @@ -103,6 +105,7 @@ func (f *FeatureFlags) SetDefaults() { f.BoardsProduct = false f.PlanUpgradeButtonText = "upgrade" f.PostPriority = false + f.PeopleProduct = false } func (f *FeatureFlags) Plugins() map[string]string { From 542c36bd6804d074e789aa3c5d02a165a64bfd6f Mon Sep 17 00:00:00 2001 From: Muhammad Salman Date: Sun, 11 Sep 2022 12:08:30 +0300 Subject: [PATCH 02/63] new ui for email verify change email address --- app/email/email.go | 4 + i18n/en.json | 2 +- templates/email_change_verify_body.html | 231 ++++++++++++++++++++---- 3 files changed, 199 insertions(+), 38 deletions(-) diff --git a/app/email/email.go b/app/email/email.go index b9057800fa..d9f3882c61 100644 --- a/app/email/email.go +++ b/app/email/email.go @@ -69,6 +69,10 @@ func (es *Service) SendEmailChangeVerifyEmail(newUserEmail, locale, siteURL, tok map[string]any{"TeamDisplayName": es.config().TeamSettings.SiteName}) data.Props["VerifyUrl"] = link data.Props["VerifyButton"] = T("api.templates.email_change_verify_body.button") + data.Props["QuestionTitle"] = T("api.templates.questions_footer.title") + data.Props["EmailInfo1"] = T("api.templates.email_us_anytime_at") + data.Props["SupportEmail"] = "feedback@mattermost.com" + data.Props["FooterV2"] = T("api.templates.email_footer_v2") body, err := es.templatesContainer.RenderToString("email_change_verify_body", data) if err != nil { diff --git a/i18n/en.json b/i18n/en.json index 5ebcc304a4..4a13acd640 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -3445,7 +3445,7 @@ }, { "id": "api.templates.email_change_verify_body.title", - "translation": "You updated your email" + "translation": "You successfully updated your email" }, { "id": "api.templates.email_change_verify_subject", diff --git a/templates/email_change_verify_body.html b/templates/email_change_verify_body.html index 0babf28737..7febc26063 100644 --- a/templates/email_change_verify_body.html +++ b/templates/email_change_verify_body.html @@ -1,46 +1,203 @@ {{define "email_change_verify_body"}} - - - - + + +
- - - - -
- + + + + + + + +
+
+
+ + + - -
+
+ + - - - - - - {{template "email_footer" . }} - +
- + +
+ + + + + + +
+ + + + + + +
+ +
+
+
- - - - - - {{template "email_info" . }} - -
-

{{.Props.Title}}

-

{{.Props.Info}}

-

- {{.Props.VerifyButton}} -

-
-
-
-
+ +
+ + + + + + +
+
+ + + + + + + + + + + + +
+

{{.Props.Title}}

+
+
{{.Props.Info}}
+
+ +
+
+
+
+
+ + + + + + +
+
+ + + + + + +
+ + + + + + +
+ +
+
+
+
+
+
+ + + + + + +
+
+ + + + + + +
+

{{.Props.QuestionTitle}}

+

{{.Props.EmailInfo1}}{{.Props.SupportEmail}}

+
+
+
+
+
+ + + + + + +
+
+ + + + + + +
+

+ {{.Props.FooterV2}} +

+
+
+
+
+
+ + {{end}} From 979359ade6830579e7e2645c92a9d02a65da44ee Mon Sep 17 00:00:00 2001 From: Muhammad Salman Date: Mon, 3 Oct 2022 17:07:19 +0300 Subject: [PATCH 03/63] [MM-44790] fix the external link warning --- templates/email_change_verify_body.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/email_change_verify_body.html b/templates/email_change_verify_body.html index 7febc26063..d2b9d318ca 100644 --- a/templates/email_change_verify_body.html +++ b/templates/email_change_verify_body.html @@ -101,7 +101,7 @@ From affddb65dabfaced1dd1c4343b8678b22b6939e9 Mon Sep 17 00:00:00 2001 From: Muhammad Salman Date: Mon, 3 Oct 2022 17:27:33 +0300 Subject: [PATCH 04/63] [MM-44790] add additional font family for consitency --- templates/email_change_verify_body.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/email_change_verify_body.html b/templates/email_change_verify_body.html index d2b9d318ca..a98286e8b0 100644 --- a/templates/email_change_verify_body.html +++ b/templates/email_change_verify_body.html @@ -157,8 +157,8 @@ From 6cc85b8350fc4c6900162ebc778e6865424cb21f Mon Sep 17 00:00:00 2001 From: Muhammad Salman Date: Mon, 3 Oct 2022 18:02:46 +0300 Subject: [PATCH 05/63] [MM-44790] removing extra quotes around font-family --- templates/email_change_verify_body.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/email_change_verify_body.html b/templates/email_change_verify_body.html index a98286e8b0..0b171b98ca 100644 --- a/templates/email_change_verify_body.html +++ b/templates/email_change_verify_body.html @@ -157,8 +157,8 @@ From 9f6497fb0edccbe9d3719fa9cdd999fa1c9295a6 Mon Sep 17 00:00:00 2001 From: Muhammad Salman Date: Tue, 11 Oct 2022 12:46:10 +0300 Subject: [PATCH 06/63] add metropolis font to email change verify email html --- templates/email_change_verify_body.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/templates/email_change_verify_body.html b/templates/email_change_verify_body.html index 0b171b98ca..8f6918bfaf 100644 --- a/templates/email_change_verify_body.html +++ b/templates/email_change_verify_body.html @@ -2,6 +2,7 @@ + @@ -87,7 +88,7 @@ From f72540c68f78c96dd3892ac82c4c61ce2b6856c3 Mon Sep 17 00:00:00 2001 From: Amy Blais <29708087+amyblais@users.noreply.github.com> Date: Tue, 11 Oct 2022 09:02:11 -0400 Subject: [PATCH 07/63] Update supported versions (#21143) Automatic Merge --- i18n/en.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/i18n/en.json b/i18n/en.json index 5ebcc304a4..0d164e18b0 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -9473,11 +9473,11 @@ }, { "id": "web.error.unsupported_browser.min_browser_version.chrome", - "translation": "Version 100+" + "translation": "Version 102+" }, { "id": "web.error.unsupported_browser.min_browser_version.edge", - "translation": "Version 44+" + "translation": "Version 95+" }, { "id": "web.error.unsupported_browser.min_browser_version.firefox", From 79651874ea43d4ef73c278b7bb19c9cfb0d30002 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Espino=20Garc=C3=ADa?= Date: Tue, 11 Oct 2022 15:33:29 +0200 Subject: [PATCH 08/63] Do not send categories for teams deleted or not member of (#21038) * Do not send categories for teams deleted or not member of * Fix tests and add test for deleted teams and memberships --- store/sqlstore/channel_store_categories.go | 7 + store/storetest/channel_store_categories.go | 457 ++++++++++++-------- 2 files changed, 282 insertions(+), 182 deletions(-) diff --git a/store/sqlstore/channel_store_categories.go b/store/sqlstore/channel_store_categories.go index 2a1799b01a..27317fe3cf 100644 --- a/store/sqlstore/channel_store_categories.go +++ b/store/sqlstore/channel_store_categories.go @@ -535,6 +535,13 @@ func (s SqlChannelStore) getSidebarCategoriesT(db dbSelecter, userId string, opt Select("SidebarCategories.*", "SidebarChannels.ChannelId"). From("SidebarCategories"). LeftJoin("SidebarChannels ON SidebarChannels.CategoryId=Id"). + InnerJoin("Teams ON Teams.Id=SidebarCategories.TeamId"). + InnerJoin("TeamMembers ON TeamMembers.TeamId=SidebarCategories.TeamId"). + Where(sq.And{ + sq.Eq{"TeamMembers.UserId": userId}, + sq.Eq{"TeamMembers.DeleteAt": 0}, + sq.Eq{"Teams.DeleteAt": 0}, + }). Where(sq.And{ sq.Eq{"SidebarCategories.UserId": userId}, }). diff --git a/store/storetest/channel_store_categories.go b/store/storetest/channel_store_categories.go index faaa0dd75c..5b8e47bab2 100644 --- a/store/storetest/channel_store_categories.go +++ b/store/storetest/channel_store_categories.go @@ -28,13 +28,38 @@ func TestChannelStoreCategories(t *testing.T, ss store.Store, s SqlStore) { t.Run("SidebarCategoryDeadlock", func(t *testing.T) { testSidebarCategoryDeadlock(t, ss) }) } +func setupTeam(t *testing.T, ss store.Store, userIds ...string) *model.Team { + team, err := ss.Team().Save(&model.Team{ + DisplayName: "Name", + Name: NewTestId(), + Email: MakeEmail(), + Type: model.TeamOpen, + }) + assert.NoError(t, err) + + members := make([]*model.TeamMember, 0, len(userIds)) + for _, userId := range userIds { + members = append(members, &model.TeamMember{ + TeamId: team.Id, + UserId: userId, + }) + } + if len(members) > 0 { + _, err = ss.Team().SaveMultipleMembers(members, len(userIds)+1) + assert.NoError(t, err) + } + + return team +} + func testCreateInitialSidebarCategories(t *testing.T, ss store.Store) { t.Run("should create initial favorites/channels/DMs categories", func(t *testing.T) { userId := model.NewId() - teamId := model.NewId() + + team := setupTeam(t, ss, userId) opts := &store.SidebarCategorySearchOpts{ - TeamID: teamId, + TeamID: team.Id, ExcludeTeam: false, } @@ -45,25 +70,25 @@ func testCreateInitialSidebarCategories(t *testing.T, ss store.Store) { assert.Equal(t, model.SidebarCategoryChannels, res.Categories[1].Type) assert.Equal(t, model.SidebarCategoryDirectMessages, res.Categories[2].Type) - res2, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, teamId) + res2, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id) assert.NoError(t, err) assert.Equal(t, res, res2) }) t.Run("should create initial favorites/channels/DMs categories for multiple users", func(t *testing.T) { userId := model.NewId() - teamId := model.NewId() + userId2 := model.NewId() + + team := setupTeam(t, ss, userId, userId2) opts := &store.SidebarCategorySearchOpts{ - TeamID: teamId, + TeamID: team.Id, ExcludeTeam: false, } res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts) require.NoError(t, nErr) require.NotEmpty(t, res) - userId2 := model.NewId() - res, nErr = ss.Channel().CreateInitialSidebarCategories(userId2, opts) assert.NoError(t, nErr) assert.Len(t, res.Categories, 3) @@ -71,26 +96,27 @@ func testCreateInitialSidebarCategories(t *testing.T, ss store.Store) { assert.Equal(t, model.SidebarCategoryChannels, res.Categories[1].Type) assert.Equal(t, model.SidebarCategoryDirectMessages, res.Categories[2].Type) - res2, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId2, teamId) + res2, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId2, team.Id) assert.NoError(t, err) assert.Equal(t, res, res2) }) t.Run("should create initial favorites/channels/DMs categories on different teams", func(t *testing.T) { userId := model.NewId() - teamId := model.NewId() + + team := setupTeam(t, ss, userId) + team2 := setupTeam(t, ss, userId) opts := &store.SidebarCategorySearchOpts{ - TeamID: teamId, + TeamID: team.Id, ExcludeTeam: false, } res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts) require.NoError(t, nErr) require.NotEmpty(t, res) - teamId2 := model.NewId() opts = &store.SidebarCategorySearchOpts{ - TeamID: teamId2, + TeamID: team2.Id, ExcludeTeam: false, } res, nErr = ss.Channel().CreateInitialSidebarCategories(userId, opts) @@ -100,24 +126,25 @@ func testCreateInitialSidebarCategories(t *testing.T, ss store.Store) { assert.Equal(t, model.SidebarCategoryChannels, res.Categories[1].Type) assert.Equal(t, model.SidebarCategoryDirectMessages, res.Categories[2].Type) - res2, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, teamId2) + res2, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team2.Id) assert.NoError(t, err) assert.Equal(t, res, res2) }) t.Run("shouldn't create additional categories when ones already exist", func(t *testing.T) { userId := model.NewId() - teamId := model.NewId() + + team := setupTeam(t, ss, userId) opts := &store.SidebarCategorySearchOpts{ - TeamID: teamId, + TeamID: team.Id, ExcludeTeam: false, } res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts) require.NoError(t, nErr) require.NotEmpty(t, res) - initialCategories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, teamId) + initialCategories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id) require.NoError(t, err) require.Equal(t, res, initialCategories) @@ -126,14 +153,15 @@ func testCreateInitialSidebarCategories(t *testing.T, ss store.Store) { assert.NoError(t, nErr) assert.NotEmpty(t, res) - res, err = ss.Channel().GetSidebarCategoriesForTeamForUser(userId, teamId) + res, err = ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id) assert.NoError(t, err) assert.Equal(t, initialCategories.Categories, res.Categories) }) t.Run("shouldn't create additional categories when ones already exist even when ran simultaneously", func(t *testing.T) { userId := model.NewId() - teamId := model.NewId() + + team := setupTeam(t, ss, userId) var wg sync.WaitGroup @@ -144,7 +172,7 @@ func testCreateInitialSidebarCategories(t *testing.T, ss store.Store) { defer wg.Done() opts := &store.SidebarCategorySearchOpts{ - TeamID: teamId, + TeamID: team.Id, ExcludeTeam: false, } _, _ = ss.Channel().CreateInitialSidebarCategories(userId, opts) @@ -153,18 +181,19 @@ func testCreateInitialSidebarCategories(t *testing.T, ss store.Store) { wg.Wait() - res, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, teamId) + res, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id) assert.NoError(t, err) assert.Len(t, res.Categories, 3) }) t.Run("should populate the Favorites category with regular channels", func(t *testing.T) { userId := model.NewId() - teamId := model.NewId() + + team := setupTeam(t, ss, userId) // Set up two channels, one favorited and one not channel1, nErr := ss.Channel().Save(&model.Channel{ - TeamId: teamId, + TeamId: team.Id, Type: model.ChannelTypeOpen, Name: "channel1", }, 1000) @@ -177,7 +206,7 @@ func testCreateInitialSidebarCategories(t *testing.T, ss store.Store) { require.NoError(t, err) channel2, nErr := ss.Channel().Save(&model.Channel{ - TeamId: teamId, + TeamId: team.Id, Type: model.ChannelTypeOpen, Name: "channel2", }, 1000) @@ -201,7 +230,7 @@ func testCreateInitialSidebarCategories(t *testing.T, ss store.Store) { // Create the categories opts := &store.SidebarCategorySearchOpts{ - TeamID: teamId, + TeamID: team.Id, ExcludeTeam: false, } categories, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts) @@ -213,18 +242,19 @@ func testCreateInitialSidebarCategories(t *testing.T, ss store.Store) { assert.Equal(t, []string{channel2.Id}, categories.Categories[1].Channels) // Get and check the categories for channels - categories2, nErr := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, teamId) + categories2, nErr := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id) require.NoError(t, nErr) require.Equal(t, categories, categories2) }) t.Run("should populate the Favorites category in alphabetical order", func(t *testing.T) { userId := model.NewId() - teamId := model.NewId() + + team := setupTeam(t, ss, userId) // Set up two channels channel1, nErr := ss.Channel().Save(&model.Channel{ - TeamId: teamId, + TeamId: team.Id, Type: model.ChannelTypeOpen, Name: "channel1", DisplayName: "zebra", @@ -238,7 +268,7 @@ func testCreateInitialSidebarCategories(t *testing.T, ss store.Store) { require.NoError(t, err) channel2, nErr := ss.Channel().Save(&model.Channel{ - TeamId: teamId, + TeamId: team.Id, Type: model.ChannelTypeOpen, Name: "channel2", DisplayName: "aardvark", @@ -269,7 +299,7 @@ func testCreateInitialSidebarCategories(t *testing.T, ss store.Store) { // Create the categories opts := &store.SidebarCategorySearchOpts{ - TeamID: teamId, + TeamID: team.Id, ExcludeTeam: false, } categories, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts) @@ -279,14 +309,15 @@ func testCreateInitialSidebarCategories(t *testing.T, ss store.Store) { assert.Equal(t, []string{channel2.Id, channel1.Id}, categories.Categories[0].Channels) // Get and check the categories for channels - categories2, nErr := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, teamId) + categories2, nErr := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id) require.NoError(t, nErr) require.Equal(t, categories, categories2) }) t.Run("should populate the Favorites category with DMs and GMs", func(t *testing.T) { userId := model.NewId() - teamId := model.NewId() + + team := setupTeam(t, ss, userId) otherUserId1 := model.NewId() otherUserId2 := model.NewId() @@ -336,7 +367,7 @@ func testCreateInitialSidebarCategories(t *testing.T, ss store.Store) { // Create the categories opts := &store.SidebarCategorySearchOpts{ - TeamID: teamId, + TeamID: team.Id, ExcludeTeam: false, } categories, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts) @@ -348,19 +379,20 @@ func testCreateInitialSidebarCategories(t *testing.T, ss store.Store) { assert.Equal(t, []string{dmChannel2.Id}, categories.Categories[2].Channels) // Get and check the categories for channels - categories2, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, teamId) + categories2, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id) require.NoError(t, err) require.Equal(t, categories, categories2) }) t.Run("should not populate the Favorites category with channels from other teams", func(t *testing.T) { userId := model.NewId() - teamId := model.NewId() - teamId2 := model.NewId() + + team := setupTeam(t, ss, userId) + team2 := setupTeam(t, ss, userId) // Set up a channel on another team and favorite it channel1, nErr := ss.Channel().Save(&model.Channel{ - TeamId: teamId2, + TeamId: team2.Id, Type: model.ChannelTypeOpen, Name: "channel1", }, 1000) @@ -384,7 +416,7 @@ func testCreateInitialSidebarCategories(t *testing.T, ss store.Store) { // Create the categories opts := &store.SidebarCategorySearchOpts{ - TeamID: teamId, + TeamID: team.Id, ExcludeTeam: false, } categories, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts) @@ -396,7 +428,7 @@ func testCreateInitialSidebarCategories(t *testing.T, ss store.Store) { assert.Equal(t, []string{}, categories.Categories[1].Channels) // Get and check the categories for channels - categories2, nErr := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, teamId) + categories2, nErr := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id) require.NoError(t, nErr) require.Equal(t, categories, categories2) }) @@ -465,10 +497,11 @@ func testCreateSidebarCategory(t *testing.T, ss store.Store) { t.Run("should place the new category second if Favorites comes first", func(t *testing.T) { userId := model.NewId() - teamId := model.NewId() + + team := setupTeam(t, ss, userId) opts := &store.SidebarCategorySearchOpts{ - TeamID: teamId, + TeamID: team.Id, ExcludeTeam: false, } res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts) @@ -476,7 +509,7 @@ func testCreateSidebarCategory(t *testing.T, ss store.Store) { require.NotEmpty(t, res) // Create the category - created, err := ss.Channel().CreateSidebarCategory(userId, teamId, &model.SidebarCategoryWithChannels{ + created, err := ss.Channel().CreateSidebarCategory(userId, team.Id, &model.SidebarCategoryWithChannels{ SidebarCategory: model.SidebarCategory{ DisplayName: model.NewId(), }, @@ -484,7 +517,7 @@ func testCreateSidebarCategory(t *testing.T, ss store.Store) { require.NoError(t, err) // Confirm that it comes second - res, err = ss.Channel().GetSidebarCategoriesForTeamForUser(userId, teamId) + res, err = ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id) require.NoError(t, err) require.Len(t, res.Categories, 4) assert.Equal(t, model.SidebarCategoryFavorites, res.Categories[0].Type) @@ -494,10 +527,11 @@ func testCreateSidebarCategory(t *testing.T, ss store.Store) { t.Run("should place the new category first if Favorites is not first", func(t *testing.T) { userId := model.NewId() - teamId := model.NewId() + + team := setupTeam(t, ss, userId) opts := &store.SidebarCategorySearchOpts{ - TeamID: teamId, + TeamID: team.Id, ExcludeTeam: false, } res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts) @@ -505,12 +539,12 @@ func testCreateSidebarCategory(t *testing.T, ss store.Store) { require.NotEmpty(t, res) // Re-arrange the categories so that Favorites comes last - categories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, teamId) + categories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id) require.NoError(t, err) require.Len(t, categories.Categories, 3) require.Equal(t, model.SidebarCategoryFavorites, categories.Categories[0].Type) - err = ss.Channel().UpdateSidebarCategoryOrder(userId, teamId, []string{ + err = ss.Channel().UpdateSidebarCategoryOrder(userId, team.Id, []string{ categories.Categories[1].Id, categories.Categories[2].Id, categories.Categories[0].Id, @@ -518,7 +552,7 @@ func testCreateSidebarCategory(t *testing.T, ss store.Store) { require.NoError(t, err) // Create the category - created, err := ss.Channel().CreateSidebarCategory(userId, teamId, &model.SidebarCategoryWithChannels{ + created, err := ss.Channel().CreateSidebarCategory(userId, team.Id, &model.SidebarCategoryWithChannels{ SidebarCategory: model.SidebarCategory{ DisplayName: model.NewId(), }, @@ -526,7 +560,7 @@ func testCreateSidebarCategory(t *testing.T, ss store.Store) { require.NoError(t, err) // Confirm that it comes first - res, err = ss.Channel().GetSidebarCategoriesForTeamForUser(userId, teamId) + res, err = ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id) require.NoError(t, err) require.Len(t, res.Categories, 4) assert.Equal(t, model.SidebarCategoryCustom, res.Categories[0].Type) @@ -535,10 +569,10 @@ func testCreateSidebarCategory(t *testing.T, ss store.Store) { t.Run("should create the category with its channels", func(t *testing.T) { userId := model.NewId() - teamId := model.NewId() + team := setupTeam(t, ss, userId) opts := &store.SidebarCategorySearchOpts{ - TeamID: teamId, + TeamID: team.Id, ExcludeTeam: false, } res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts) @@ -548,19 +582,19 @@ func testCreateSidebarCategory(t *testing.T, ss store.Store) { // Create some channels channel1, err := ss.Channel().Save(&model.Channel{ Type: model.ChannelTypeOpen, - TeamId: teamId, + TeamId: team.Id, Name: model.NewId(), }, 100) require.NoError(t, err) channel2, err := ss.Channel().Save(&model.Channel{ Type: model.ChannelTypeOpen, - TeamId: teamId, + TeamId: team.Id, Name: model.NewId(), }, 100) require.NoError(t, err) // Create the category - created, err := ss.Channel().CreateSidebarCategory(userId, teamId, &model.SidebarCategoryWithChannels{ + created, err := ss.Channel().CreateSidebarCategory(userId, team.Id, &model.SidebarCategoryWithChannels{ SidebarCategory: model.SidebarCategory{ DisplayName: model.NewId(), }, @@ -577,17 +611,17 @@ func testCreateSidebarCategory(t *testing.T, ss store.Store) { t.Run("should remove any channels from their previous categories", func(t *testing.T) { userId := model.NewId() - teamId := model.NewId() + team := setupTeam(t, ss, userId) opts := &store.SidebarCategorySearchOpts{ - TeamID: teamId, + TeamID: team.Id, ExcludeTeam: false, } res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts) require.NoError(t, nErr) require.NotEmpty(t, res) - categories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, teamId) + categories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id) require.NoError(t, err) require.Len(t, categories.Categories, 3) @@ -599,13 +633,13 @@ func testCreateSidebarCategory(t *testing.T, ss store.Store) { // Create some channels channel1, nErr := ss.Channel().Save(&model.Channel{ Type: model.ChannelTypeOpen, - TeamId: teamId, + TeamId: team.Id, Name: model.NewId(), }, 100) require.NoError(t, nErr) channel2, nErr := ss.Channel().Save(&model.Channel{ Type: model.ChannelTypeOpen, - TeamId: teamId, + TeamId: team.Id, Name: model.NewId(), }, 100) require.NoError(t, nErr) @@ -613,14 +647,14 @@ func testCreateSidebarCategory(t *testing.T, ss store.Store) { // Assign them to categories favoritesCategory.Channels = []string{channel1.Id} channelsCategory.Channels = []string{channel2.Id} - _, _, err = ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ + _, _, err = ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{ favoritesCategory, channelsCategory, }) require.NoError(t, err) // Create the category - created, err := ss.Channel().CreateSidebarCategory(userId, teamId, &model.SidebarCategoryWithChannels{ + created, err := ss.Channel().CreateSidebarCategory(userId, team.Id, &model.SidebarCategoryWithChannels{ SidebarCategory: model.SidebarCategory{ DisplayName: model.NewId(), }, @@ -643,14 +677,14 @@ func testCreateSidebarCategory(t *testing.T, ss store.Store) { func testGetSidebarCategory(t *testing.T, ss store.Store, s SqlStore) { t.Run("should return a custom category with its Channels field set", func(t *testing.T) { userId := model.NewId() - teamId := model.NewId() + team := setupTeam(t, ss, userId) channelId1 := model.NewId() channelId2 := model.NewId() channelId3 := model.NewId() opts := &store.SidebarCategorySearchOpts{ - TeamID: teamId, + TeamID: team.Id, ExcludeTeam: false, } res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts) @@ -658,10 +692,10 @@ func testGetSidebarCategory(t *testing.T, ss store.Store, s SqlStore) { require.NotEmpty(t, res) // Create a category and assign some channels to it - created, err := ss.Channel().CreateSidebarCategory(userId, teamId, &model.SidebarCategoryWithChannels{ + created, err := ss.Channel().CreateSidebarCategory(userId, team.Id, &model.SidebarCategoryWithChannels{ SidebarCategory: model.SidebarCategory{ UserId: userId, - TeamId: teamId, + TeamId: team.Id, DisplayName: model.NewId(), }, Channels: []string{channelId1, channelId2, channelId3}, @@ -680,18 +714,18 @@ func testGetSidebarCategory(t *testing.T, ss store.Store, s SqlStore) { t.Run("should return any orphaned channels with the Channels category", func(t *testing.T) { userId := model.NewId() - teamId := model.NewId() + team := setupTeam(t, ss, userId) // Create the initial categories and find the channels category opts := &store.SidebarCategorySearchOpts{ - TeamID: teamId, + TeamID: team.Id, ExcludeTeam: false, } res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts) require.NoError(t, nErr) require.NotEmpty(t, res) - categories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, teamId) + categories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id) require.NoError(t, err) channelsCategory := categories.Categories[1] @@ -701,7 +735,7 @@ func testGetSidebarCategory(t *testing.T, ss store.Store, s SqlStore) { channel1, nErr := ss.Channel().Save(&model.Channel{ Name: "channel1", DisplayName: "DEF", - TeamId: teamId, + TeamId: team.Id, Type: model.ChannelTypePrivate, }, 10) require.NoError(t, nErr) @@ -715,7 +749,7 @@ func testGetSidebarCategory(t *testing.T, ss store.Store, s SqlStore) { channel2, nErr := ss.Channel().Save(&model.Channel{ Name: "channel2", DisplayName: "ABC", - TeamId: teamId, + TeamId: team.Id, Type: model.ChannelTypeOpen, }, 10) require.NoError(t, nErr) @@ -748,18 +782,18 @@ func testGetSidebarCategory(t *testing.T, ss store.Store, s SqlStore) { t.Run("shouldn't return orphaned channels on another team with the Channels category", func(t *testing.T) { userId := model.NewId() - teamId := model.NewId() + team := setupTeam(t, ss, userId) // Create the initial categories and find the channels category opts := &store.SidebarCategorySearchOpts{ - TeamID: teamId, + TeamID: team.Id, ExcludeTeam: false, } res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts) require.NoError(t, nErr) require.NotEmpty(t, res) - categories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, teamId) + categories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id) require.NoError(t, err) require.Equal(t, model.SidebarCategoryChannels, categories.Categories[1].Type) @@ -791,10 +825,10 @@ func testGetSidebarCategory(t *testing.T, ss store.Store, s SqlStore) { t.Run("shouldn't return non-orphaned channels with the Channels category", func(t *testing.T) { userId := model.NewId() - teamId := model.NewId() + team := setupTeam(t, ss, userId) opts := &store.SidebarCategorySearchOpts{ - TeamID: teamId, + TeamID: team.Id, ExcludeTeam: false, } // Create the initial categories and find the channels category @@ -802,7 +836,7 @@ func testGetSidebarCategory(t *testing.T, ss store.Store, s SqlStore) { require.NoError(t, nErr) require.NotEmpty(t, res) - categories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, teamId) + categories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id) require.NoError(t, err) favoritesCategory := categories.Categories[0] @@ -814,7 +848,7 @@ func testGetSidebarCategory(t *testing.T, ss store.Store, s SqlStore) { channel1, nErr := ss.Channel().Save(&model.Channel{ Name: "channel1", DisplayName: "DEF", - TeamId: teamId, + TeamId: team.Id, Type: model.ChannelTypePrivate, }, 10) require.NoError(t, nErr) @@ -828,7 +862,7 @@ func testGetSidebarCategory(t *testing.T, ss store.Store, s SqlStore) { channel2, nErr := ss.Channel().Save(&model.Channel{ Name: "channel2", DisplayName: "ABC", - TeamId: teamId, + TeamId: team.Id, Type: model.ChannelTypeOpen, }, 10) require.NoError(t, nErr) @@ -840,7 +874,7 @@ func testGetSidebarCategory(t *testing.T, ss store.Store, s SqlStore) { require.NoError(t, nErr) // And assign one to another category - _, _, err = ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ + _, _, err = ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{ { SidebarCategory: favoritesCategory.SidebarCategory, Channels: []string{channel2.Id}, @@ -858,18 +892,18 @@ func testGetSidebarCategory(t *testing.T, ss store.Store, s SqlStore) { t.Run("should return any orphaned DM channels with the Direct Messages category", func(t *testing.T) { userId := model.NewId() - teamId := model.NewId() + team := setupTeam(t, ss, userId) // Create the initial categories and find the DMs category opts := &store.SidebarCategorySearchOpts{ - TeamID: teamId, + TeamID: team.Id, ExcludeTeam: false, } res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts) require.NoError(t, nErr) require.NotEmpty(t, res) - categories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, teamId) + categories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id) require.NoError(t, err) require.Equal(t, model.SidebarCategoryDirectMessages, categories.Categories[2].Type) @@ -903,18 +937,18 @@ func testGetSidebarCategory(t *testing.T, ss store.Store, s SqlStore) { t.Run("should return any orphaned GM channels with the Direct Messages category", func(t *testing.T) { userId := model.NewId() - teamId := model.NewId() + team := setupTeam(t, ss, userId) // Create the initial categories and find the DMs category opts := &store.SidebarCategorySearchOpts{ - TeamID: teamId, + TeamID: team.Id, ExcludeTeam: false, } res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts) require.NoError(t, nErr) require.NotEmpty(t, res) - categories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, teamId) + categories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id) require.NoError(t, err) require.Equal(t, model.SidebarCategoryDirectMessages, categories.Categories[2].Type) @@ -945,18 +979,18 @@ func testGetSidebarCategory(t *testing.T, ss store.Store, s SqlStore) { t.Run("should return orphaned DM channels in the DMs category which are in a custom category on another team", func(t *testing.T) { userId := model.NewId() - teamId := model.NewId() + team := setupTeam(t, ss, userId) // Create the initial categories and find the DMs category opts := &store.SidebarCategorySearchOpts{ - TeamID: teamId, + TeamID: team.Id, ExcludeTeam: false, } res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts) require.NoError(t, nErr) require.NotEmpty(t, res) - categories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, teamId) + categories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id) require.NoError(t, err) require.Equal(t, model.SidebarCategoryDirectMessages, categories.Categories[2].Type) @@ -981,19 +1015,19 @@ func testGetSidebarCategory(t *testing.T, ss store.Store, s SqlStore) { require.NoError(t, nErr) // Create another team and assign the DM to a custom category on that team - otherTeamId := model.NewId() + otherTeam := setupTeam(t, ss, userId) opts = &store.SidebarCategorySearchOpts{ - TeamID: otherTeamId, + TeamID: otherTeam.Id, ExcludeTeam: false, } res, nErr = ss.Channel().CreateInitialSidebarCategories(userId, opts) require.NoError(t, nErr) require.NotEmpty(t, res) - _, err = ss.Channel().CreateSidebarCategory(userId, otherTeamId, &model.SidebarCategoryWithChannels{ + _, err = ss.Channel().CreateSidebarCategory(userId, otherTeam.Id, &model.SidebarCategoryWithChannels{ SidebarCategory: model.SidebarCategory{ UserId: userId, - TeamId: teamId, + TeamId: team.Id, }, Channels: []string{dmChannel.Id}, }) @@ -1011,10 +1045,10 @@ func testGetSidebarCategory(t *testing.T, ss store.Store, s SqlStore) { func testGetSidebarCategories(t *testing.T, ss store.Store) { t.Run("should return channels in the same order between different ways of getting categories", func(t *testing.T) { userId := model.NewId() - teamId := model.NewId() + team := setupTeam(t, ss, userId) opts := &store.SidebarCategorySearchOpts{ - TeamID: teamId, + TeamID: team.Id, ExcludeTeam: false, } res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts) @@ -1027,7 +1061,7 @@ func testGetSidebarCategories(t *testing.T, ss store.Store) { model.NewId(), } - newCategory, err := ss.Channel().CreateSidebarCategory(userId, teamId, &model.SidebarCategoryWithChannels{ + newCategory, err := ss.Channel().CreateSidebarCategory(userId, team.Id, &model.SidebarCategoryWithChannels{ Channels: channelIds, }) require.NoError(t, err) @@ -1036,7 +1070,7 @@ func testGetSidebarCategories(t *testing.T, ss store.Store) { gotCategory, err := ss.Channel().GetSidebarCategory(newCategory.Id) require.NoError(t, err) - res, err = ss.Channel().GetSidebarCategoriesForTeamForUser(userId, teamId) + res, err = ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id) require.NoError(t, err) require.Len(t, res.Categories, 4) @@ -1047,23 +1081,83 @@ func testGetSidebarCategories(t *testing.T, ss store.Store) { assert.Equal(t, gotCategory.Channels, res.Categories[1].Channels) assert.Equal(t, channelIds, res.Categories[1].Channels) }) + t.Run("should not return categories for teams deleted, or no longer a member", func(t *testing.T) { + userId := model.NewId() + + teamMember1 := setupTeam(t, ss, userId) + teamMember2 := setupTeam(t, ss, userId) + teamDeleted := setupTeam(t, ss, userId) + teamDeleted.DeleteAt = model.GetMillis() + ss.Team().Update(teamDeleted) + teamNotMember := setupTeam(t, ss) + teamDeletedMember := setupTeam(t, ss, userId) + + members, err := ss.Team().GetMembersByIds(teamDeletedMember.Id, []string{userId}, nil) + require.NoError(t, err) + require.NotEmpty(t, members) + member := members[0] + member.DeleteAt = model.GetMillis() + ss.Team().UpdateMember(member) + + teamIds := []string{ + teamMember1.Id, + teamMember2.Id, + teamDeleted.Id, + teamNotMember.Id, + teamDeletedMember.Id, + } + + for _, id := range teamIds { + res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, &store.SidebarCategorySearchOpts{TeamID: id}) + require.NoError(t, nErr) + require.NotEmpty(t, res) + } + + opts := &store.SidebarCategorySearchOpts{ + TeamID: teamMember1.Id, + ExcludeTeam: false, + } + + // Team member and not exclude + res, err := ss.Channel().GetSidebarCategories(userId, opts) + require.NoError(t, err) + assert.Equal(t, 3, len(res.Categories)) + + // No team member and not exclude + opts.TeamID = teamDeleted.Id + res, err = ss.Channel().GetSidebarCategories(userId, opts) + require.NoError(t, err) + assert.Equal(t, 0, len(res.Categories)) + + // No team member and exclude + opts.ExcludeTeam = true + res, err = ss.Channel().GetSidebarCategories(userId, opts) + require.NoError(t, err) + assert.Equal(t, 6, len(res.Categories)) + + // Team member and exclude + opts.TeamID = teamMember1.Id + res, err = ss.Channel().GetSidebarCategories(userId, opts) + require.NoError(t, err) + assert.Equal(t, 3, len(res.Categories)) + }) } func testUpdateSidebarCategories(t *testing.T, ss store.Store) { t.Run("ensure the query to update SidebarCategories hasn't been polluted by UpdateSidebarCategoryOrder", func(t *testing.T) { userId := model.NewId() - teamId := model.NewId() + team := setupTeam(t, ss, userId) // Create the initial categories opts := &store.SidebarCategorySearchOpts{ - TeamID: teamId, + TeamID: team.Id, ExcludeTeam: false, } res, err := ss.Channel().CreateInitialSidebarCategories(userId, opts) require.NoError(t, err) require.NotEmpty(t, res) - initialCategories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, teamId) + initialCategories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id) require.NoError(t, err) favoritesCategory := initialCategories.Categories[0] @@ -1071,7 +1165,7 @@ func testUpdateSidebarCategories(t *testing.T, ss store.Store) { dmsCategory := initialCategories.Categories[2] // And then update one of them - updated, _, err := ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ + updated, _, err := ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{ channelsCategory, }) require.NoError(t, err) @@ -1079,7 +1173,7 @@ func testUpdateSidebarCategories(t *testing.T, ss store.Store) { assert.Equal(t, "Channels", updated[0].DisplayName) // And then reorder the categories - err = ss.Channel().UpdateSidebarCategoryOrder(userId, teamId, []string{dmsCategory.Id, favoritesCategory.Id, channelsCategory.Id}) + err = ss.Channel().UpdateSidebarCategoryOrder(userId, team.Id, []string{dmsCategory.Id, favoritesCategory.Id, channelsCategory.Id}) require.NoError(t, err) // Which somehow blanks out stuff because ??? @@ -1090,18 +1184,18 @@ func testUpdateSidebarCategories(t *testing.T, ss store.Store) { t.Run("categories should be returned in their original order", func(t *testing.T) { userId := model.NewId() - teamId := model.NewId() + team := setupTeam(t, ss, userId) // Create the initial categories opts := &store.SidebarCategorySearchOpts{ - TeamID: teamId, + TeamID: team.Id, ExcludeTeam: false, } res, err := ss.Channel().CreateInitialSidebarCategories(userId, opts) require.NoError(t, err) require.NotEmpty(t, res) - initialCategories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, teamId) + initialCategories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id) require.NoError(t, err) favoritesCategory := initialCategories.Categories[0] @@ -1109,7 +1203,7 @@ func testUpdateSidebarCategories(t *testing.T, ss store.Store) { dmsCategory := initialCategories.Categories[2] // And then update them - updatedCategories, _, err := ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ + updatedCategories, _, err := ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{ favoritesCategory, channelsCategory, dmsCategory, @@ -1122,24 +1216,24 @@ func testUpdateSidebarCategories(t *testing.T, ss store.Store) { t.Run("should silently fail to update read only fields", func(t *testing.T) { userId := model.NewId() - teamId := model.NewId() + team := setupTeam(t, ss, userId) opts := &store.SidebarCategorySearchOpts{ - TeamID: teamId, + TeamID: team.Id, ExcludeTeam: false, } res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts) require.NoError(t, nErr) require.NotEmpty(t, res) - initialCategories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, teamId) + initialCategories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id) require.NoError(t, err) favoritesCategory := initialCategories.Categories[0] channelsCategory := initialCategories.Categories[1] dmsCategory := initialCategories.Categories[2] - customCategory, err := ss.Channel().CreateSidebarCategory(userId, teamId, &model.SidebarCategoryWithChannels{}) + customCategory, err := ss.Channel().CreateSidebarCategory(userId, team.Id, &model.SidebarCategoryWithChannels{}) require.NoError(t, err) categoriesToUpdate := []*model.SidebarCategoryWithChannels{ @@ -1177,7 +1271,7 @@ func testUpdateSidebarCategories(t *testing.T, ss store.Store) { }, } - updatedCategories, _, err := ss.Channel().UpdateSidebarCategories(userId, teamId, categoriesToUpdate) + updatedCategories, _, err := ss.Channel().UpdateSidebarCategories(userId, team.Id, categoriesToUpdate) assert.NoError(t, err) assert.NotEqual(t, "Favorites", categoriesToUpdate[0].DisplayName) @@ -1192,18 +1286,18 @@ func testUpdateSidebarCategories(t *testing.T, ss store.Store) { t.Run("should add and remove favorites preferences based on the Favorites category", func(t *testing.T) { userId := model.NewId() - teamId := model.NewId() + team := setupTeam(t, ss, userId) // Create the initial categories and find the favorites category opts := &store.SidebarCategorySearchOpts{ - TeamID: teamId, + TeamID: team.Id, ExcludeTeam: false, } res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts) require.NoError(t, nErr) require.NotEmpty(t, res) - categories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, teamId) + categories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id) require.NoError(t, err) favoritesCategory := categories.Categories[0] @@ -1213,7 +1307,7 @@ func testUpdateSidebarCategories(t *testing.T, ss store.Store) { channel, nErr := ss.Channel().Save(&model.Channel{ Name: "channel", Type: model.ChannelTypeOpen, - TeamId: teamId, + TeamId: team.Id, }, 10) require.NoError(t, nErr) _, nErr = ss.Channel().SaveMember(&model.ChannelMember{ @@ -1224,7 +1318,7 @@ func testUpdateSidebarCategories(t *testing.T, ss store.Store) { require.NoError(t, nErr) // Assign it to favorites - _, _, err = ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ + _, _, err = ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{ { SidebarCategory: favoritesCategory.SidebarCategory, Channels: []string{channel.Id}, @@ -1241,7 +1335,7 @@ func testUpdateSidebarCategories(t *testing.T, ss store.Store) { channelsCategory := categories.Categories[1] require.Equal(t, model.SidebarCategoryChannels, channelsCategory.Type) - _, _, err = ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ + _, _, err = ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{ { SidebarCategory: channelsCategory.SidebarCategory, Channels: []string{channel.Id}, @@ -1257,18 +1351,18 @@ func testUpdateSidebarCategories(t *testing.T, ss store.Store) { t.Run("should add and remove favorites preferences for DMs", func(t *testing.T) { userId := model.NewId() - teamId := model.NewId() + team := setupTeam(t, ss, userId) // Create the initial categories and find the favorites category opts := &store.SidebarCategorySearchOpts{ - TeamID: teamId, + TeamID: team.Id, ExcludeTeam: false, } res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts) require.NoError(t, nErr) require.NotEmpty(t, res) - categories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, teamId) + categories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id) require.NoError(t, err) favoritesCategory := categories.Categories[0] @@ -1294,7 +1388,7 @@ func testUpdateSidebarCategories(t *testing.T, ss store.Store) { assert.NoError(t, nErr) // Assign it to favorites - _, _, err = ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ + _, _, err = ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{ { SidebarCategory: favoritesCategory.SidebarCategory, Channels: []string{dmChannel.Id}, @@ -1311,7 +1405,7 @@ func testUpdateSidebarCategories(t *testing.T, ss store.Store) { dmsCategory := categories.Categories[2] require.Equal(t, model.SidebarCategoryDirectMessages, dmsCategory.Type) - _, _, err = ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ + _, _, err = ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{ { SidebarCategory: dmsCategory.SidebarCategory, Channels: []string{dmChannel.Id}, @@ -1327,33 +1421,33 @@ func testUpdateSidebarCategories(t *testing.T, ss store.Store) { t.Run("should add and remove favorites preferences, even if the channel is already favorited in preferences", func(t *testing.T) { userId := model.NewId() - teamId := model.NewId() - teamId2 := model.NewId() + team := setupTeam(t, ss, userId) + team2 := setupTeam(t, ss, userId) // Create the initial categories and find the favorites categories in each team opts := &store.SidebarCategorySearchOpts{ - TeamID: teamId, + TeamID: team.Id, ExcludeTeam: false, } res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts) require.NoError(t, nErr) require.NotEmpty(t, res) - categories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, teamId) + categories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id) require.NoError(t, err) favoritesCategory := categories.Categories[0] require.Equal(t, model.SidebarCategoryFavorites, favoritesCategory.Type) opts = &store.SidebarCategorySearchOpts{ - TeamID: teamId2, + TeamID: team2.Id, ExcludeTeam: false, } res, nErr = ss.Channel().CreateInitialSidebarCategories(userId, opts) require.NoError(t, nErr) require.NotEmpty(t, res) - categories2, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, teamId2) + categories2, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team2.Id) require.NoError(t, err) favoritesCategory2 := categories2.Categories[0] @@ -1379,7 +1473,7 @@ func testUpdateSidebarCategories(t *testing.T, ss store.Store) { assert.NoError(t, nErr) // Assign it to favorites on the first team. The favorites preference gets set for all teams. - _, _, err = ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ + _, _, err = ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{ { SidebarCategory: favoritesCategory.SidebarCategory, Channels: []string{dmChannel.Id}, @@ -1393,7 +1487,7 @@ func testUpdateSidebarCategories(t *testing.T, ss store.Store) { assert.Equal(t, "true", res2.Value) // Assign it to favorites on the second team. The favorites preference is already set. - updated, _, err := ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ + updated, _, err := ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{ { SidebarCategory: favoritesCategory2.SidebarCategory, Channels: []string{dmChannel.Id}, @@ -1408,7 +1502,7 @@ func testUpdateSidebarCategories(t *testing.T, ss store.Store) { assert.Equal(t, "true", res2.Value) // Remove it from favorites on the first team. This clears the favorites preference for all teams. - _, _, err = ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ + _, _, err = ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{ { SidebarCategory: favoritesCategory.SidebarCategory, Channels: []string{}, @@ -1421,7 +1515,7 @@ func testUpdateSidebarCategories(t *testing.T, ss store.Store) { assert.Nil(t, res2) // Remove it from favorites on the second team. The favorites preference was already deleted. - _, _, err = ss.Channel().UpdateSidebarCategories(userId, teamId2, []*model.SidebarCategoryWithChannels{ + _, _, err = ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{ { SidebarCategory: favoritesCategory2.SidebarCategory, Channels: []string{}, @@ -1436,18 +1530,19 @@ func testUpdateSidebarCategories(t *testing.T, ss store.Store) { t.Run("should not affect other users' favorites preferences", func(t *testing.T) { userId := model.NewId() - teamId := model.NewId() + userId2 := model.NewId() + team := setupTeam(t, ss, userId, userId2) // Create the initial categories and find the favorites category opts := &store.SidebarCategorySearchOpts{ - TeamID: teamId, + TeamID: team.Id, ExcludeTeam: false, } res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts) require.NoError(t, nErr) require.NotEmpty(t, res) - categories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, teamId) + categories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id) require.NoError(t, err) favoritesCategory := categories.Categories[0] @@ -1456,13 +1551,11 @@ func testUpdateSidebarCategories(t *testing.T, ss store.Store) { require.Equal(t, model.SidebarCategoryChannels, channelsCategory.Type) // Create the other users' categories - userId2 := model.NewId() - res, nErr = ss.Channel().CreateInitialSidebarCategories(userId2, opts) require.NoError(t, nErr) require.NotEmpty(t, res) - categories2, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId2, teamId) + categories2, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId2, team.Id) require.NoError(t, err) favoritesCategory2 := categories2.Categories[0] @@ -1474,7 +1567,7 @@ func testUpdateSidebarCategories(t *testing.T, ss store.Store) { channel, nErr := ss.Channel().Save(&model.Channel{ Name: "channel", Type: model.ChannelTypeOpen, - TeamId: teamId, + TeamId: team.Id, }, 10) require.NoError(t, nErr) _, nErr = ss.Channel().SaveMember(&model.ChannelMember{ @@ -1491,7 +1584,7 @@ func testUpdateSidebarCategories(t *testing.T, ss store.Store) { require.NoError(t, nErr) // Have user1 favorite it - _, _, err = ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ + _, _, err = ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{ { SidebarCategory: favoritesCategory.SidebarCategory, Channels: []string{channel.Id}, @@ -1513,7 +1606,7 @@ func testUpdateSidebarCategories(t *testing.T, ss store.Store) { assert.Nil(t, res2) // And user2 favorite it - _, _, err = ss.Channel().UpdateSidebarCategories(userId2, teamId, []*model.SidebarCategoryWithChannels{ + _, _, err = ss.Channel().UpdateSidebarCategories(userId2, team.Id, []*model.SidebarCategoryWithChannels{ { SidebarCategory: favoritesCategory2.SidebarCategory, Channels: []string{channel.Id}, @@ -1536,7 +1629,7 @@ func testUpdateSidebarCategories(t *testing.T, ss store.Store) { assert.Equal(t, "true", res2.Value) // And then user1 unfavorite it - _, _, err = ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ + _, _, err = ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{ { SidebarCategory: channelsCategory.SidebarCategory, Channels: []string{channel.Id}, @@ -1558,7 +1651,7 @@ func testUpdateSidebarCategories(t *testing.T, ss store.Store) { assert.Equal(t, "true", res2.Value) // And finally user2 favorite it - _, _, err = ss.Channel().UpdateSidebarCategories(userId2, teamId, []*model.SidebarCategoryWithChannels{ + _, _, err = ss.Channel().UpdateSidebarCategories(userId2, team.Id, []*model.SidebarCategoryWithChannels{ { SidebarCategory: channelsCategory2.SidebarCategory, Channels: []string{channel.Id}, @@ -1581,13 +1674,13 @@ func testUpdateSidebarCategories(t *testing.T, ss store.Store) { t.Run("channels removed from Channels or DMs categories should be re-added", func(t *testing.T) { userId := model.NewId() - teamId := model.NewId() + team := setupTeam(t, ss, userId) // Create some channels channel, nErr := ss.Channel().Save(&model.Channel{ Name: "channel", Type: model.ChannelTypeOpen, - TeamId: teamId, + TeamId: team.Id, }, 10) require.NoError(t, nErr) _, err := ss.Channel().SaveMember(&model.ChannelMember{ @@ -1615,7 +1708,7 @@ func testUpdateSidebarCategories(t *testing.T, ss store.Store) { require.NoError(t, nErr) opts := &store.SidebarCategorySearchOpts{ - TeamID: teamId, + TeamID: team.Id, ExcludeTeam: false, } res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts) @@ -1623,7 +1716,7 @@ func testUpdateSidebarCategories(t *testing.T, ss store.Store) { require.NotEmpty(t, res) // And some categories - initialCategories, nErr := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, teamId) + initialCategories, nErr := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id) require.NoError(t, nErr) channelsCategory := initialCategories.Categories[1] @@ -1644,7 +1737,7 @@ func testUpdateSidebarCategories(t *testing.T, ss store.Store) { }, } - updatedCategories, _, nErr := ss.Channel().UpdateSidebarCategories(userId, teamId, categoriesToUpdate) + updatedCategories, _, nErr := ss.Channel().UpdateSidebarCategories(userId, team.Id, categoriesToUpdate) assert.NoError(t, nErr) // The channels should still exist in the category because they would otherwise be orphaned @@ -1654,7 +1747,7 @@ func testUpdateSidebarCategories(t *testing.T, ss store.Store) { t.Run("should be able to move DMs into and out of custom categories", func(t *testing.T) { userId := model.NewId() - teamId := model.NewId() + team := setupTeam(t, ss, userId) otherUserId := model.NewId() dmChannel, nErr := ss.Channel().SaveDirectChannel( @@ -1674,7 +1767,7 @@ func testUpdateSidebarCategories(t *testing.T, ss store.Store) { require.NoError(t, nErr) opts := &store.SidebarCategorySearchOpts{ - TeamID: teamId, + TeamID: team.Id, ExcludeTeam: false, } res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts) @@ -1682,14 +1775,14 @@ func testUpdateSidebarCategories(t *testing.T, ss store.Store) { require.NotEmpty(t, res) // The DM should start in the DMs category - initialCategories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, teamId) + initialCategories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id) require.NoError(t, err) dmsCategory := initialCategories.Categories[2] require.Equal(t, []string{dmChannel.Id}, dmsCategory.Channels) // Now move the DM into a custom category - customCategory, err := ss.Channel().CreateSidebarCategory(userId, teamId, &model.SidebarCategoryWithChannels{}) + customCategory, err := ss.Channel().CreateSidebarCategory(userId, team.Id, &model.SidebarCategoryWithChannels{}) require.NoError(t, err) categoriesToUpdate := []*model.SidebarCategoryWithChannels{ @@ -1703,7 +1796,7 @@ func testUpdateSidebarCategories(t *testing.T, ss store.Store) { }, } - updatedCategories, _, err := ss.Channel().UpdateSidebarCategories(userId, teamId, categoriesToUpdate) + updatedCategories, _, err := ss.Channel().UpdateSidebarCategories(userId, team.Id, categoriesToUpdate) assert.NoError(t, err) assert.Equal(t, dmsCategory.Id, updatedCategories[0].Id) assert.Equal(t, []string{}, updatedCategories[0].Channels) @@ -1730,7 +1823,7 @@ func testUpdateSidebarCategories(t *testing.T, ss store.Store) { }, } - updatedCategories, _, err = ss.Channel().UpdateSidebarCategories(userId, teamId, categoriesToUpdate) + updatedCategories, _, err = ss.Channel().UpdateSidebarCategories(userId, team.Id, categoriesToUpdate) assert.NoError(t, err) assert.Equal(t, dmsCategory.Id, updatedCategories[0].Id) assert.Equal(t, []string{dmChannel.Id}, updatedCategories[0].Channels) @@ -1748,13 +1841,13 @@ func testUpdateSidebarCategories(t *testing.T, ss store.Store) { t.Run("should successfully move channels between categories", func(t *testing.T) { userId := model.NewId() - teamId := model.NewId() + team := setupTeam(t, ss, userId) // Join a channel channel, nErr := ss.Channel().Save(&model.Channel{ Name: "channel", Type: model.ChannelTypeOpen, - TeamId: teamId, + TeamId: team.Id, }, 10) require.NoError(t, nErr) _, err := ss.Channel().SaveMember(&model.ChannelMember{ @@ -1766,24 +1859,24 @@ func testUpdateSidebarCategories(t *testing.T, ss store.Store) { // And then create the initial categories so that it includes the channel opts := &store.SidebarCategorySearchOpts{ - TeamID: teamId, + TeamID: team.Id, ExcludeTeam: false, } res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts) require.NoError(t, nErr) require.NotEmpty(t, res) - initialCategories, nErr := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, teamId) + initialCategories, nErr := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id) require.NoError(t, nErr) channelsCategory := initialCategories.Categories[1] require.Equal(t, []string{channel.Id}, channelsCategory.Channels) - customCategory, nErr := ss.Channel().CreateSidebarCategory(userId, teamId, &model.SidebarCategoryWithChannels{}) + customCategory, nErr := ss.Channel().CreateSidebarCategory(userId, team.Id, &model.SidebarCategoryWithChannels{}) require.NoError(t, nErr) // Move the channel one way - updatedCategories, _, nErr := ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ + updatedCategories, _, nErr := ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{ { SidebarCategory: channelsCategory.SidebarCategory, Channels: []string{}, @@ -1799,7 +1892,7 @@ func testUpdateSidebarCategories(t *testing.T, ss store.Store) { assert.Equal(t, []string{channel.Id}, updatedCategories[1].Channels) // And then the other - updatedCategories, _, nErr = ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ + updatedCategories, _, nErr = ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{ { SidebarCategory: channelsCategory.SidebarCategory, Channels: []string{channel.Id}, @@ -1816,13 +1909,13 @@ func testUpdateSidebarCategories(t *testing.T, ss store.Store) { t.Run("should correctly return the original categories that were modified", func(t *testing.T) { userId := model.NewId() - teamId := model.NewId() + team := setupTeam(t, ss, userId) // Join a channel channel, nErr := ss.Channel().Save(&model.Channel{ Name: "channel", Type: model.ChannelTypeOpen, - TeamId: teamId, + TeamId: team.Id, }, 10) require.NoError(t, nErr) _, err := ss.Channel().SaveMember(&model.ChannelMember{ @@ -1834,20 +1927,20 @@ func testUpdateSidebarCategories(t *testing.T, ss store.Store) { // And then create the initial categories so that Channels includes the channel opts := &store.SidebarCategorySearchOpts{ - TeamID: teamId, + TeamID: team.Id, ExcludeTeam: false, } res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts) require.NoError(t, nErr) require.NotEmpty(t, res) - initialCategories, nErr := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, teamId) + initialCategories, nErr := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id) require.NoError(t, nErr) channelsCategory := initialCategories.Categories[1] require.Equal(t, []string{channel.Id}, channelsCategory.Channels) - customCategory, nErr := ss.Channel().CreateSidebarCategory(userId, teamId, &model.SidebarCategoryWithChannels{ + customCategory, nErr := ss.Channel().CreateSidebarCategory(userId, team.Id, &model.SidebarCategoryWithChannels{ SidebarCategory: model.SidebarCategory{ DisplayName: "originalName", }, @@ -1855,7 +1948,7 @@ func testUpdateSidebarCategories(t *testing.T, ss store.Store) { require.NoError(t, nErr) // Rename the custom category - updatedCategories, originalCategories, nErr := ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ + updatedCategories, originalCategories, nErr := ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{ { SidebarCategory: model.SidebarCategory{ Id: customCategory.Id, @@ -1869,7 +1962,7 @@ func testUpdateSidebarCategories(t *testing.T, ss store.Store) { assert.Equal(t, "updatedName", updatedCategories[0].DisplayName) // Move a channel - updatedCategories, originalCategories, nErr = ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ + updatedCategories, originalCategories, nErr = ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{ { SidebarCategory: channelsCategory.SidebarCategory, Channels: []string{}, @@ -1893,21 +1986,21 @@ func testUpdateSidebarCategories(t *testing.T, ss store.Store) { func setupInitialSidebarCategories(t *testing.T, ss store.Store) (string, string) { userId := model.NewId() - teamId := model.NewId() + team := setupTeam(t, ss, userId) opts := &store.SidebarCategorySearchOpts{ - TeamID: teamId, + TeamID: team.Id, ExcludeTeam: false, } res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts) require.NoError(t, nErr) require.NotEmpty(t, res) - res, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, teamId) + res, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id) require.NoError(t, err) require.Len(t, res.Categories, 3) - return userId, teamId + return userId, team.Id } func testClearSidebarOnTeamLeave(t *testing.T, ss store.Store, s SqlStore) { @@ -2020,17 +2113,17 @@ func testClearSidebarOnTeamLeave(t *testing.T, ss store.Store, s SqlStore) { } // Create a second team and set up the sidebar categories for it - teamId2 := model.NewId() + team2 := setupTeam(t, ss, userId) opts := &store.SidebarCategorySearchOpts{ - TeamID: teamId2, + TeamID: team2.Id, ExcludeTeam: false, } res, err := ss.Channel().CreateInitialSidebarCategories(userId, opts) require.NoError(t, err) require.NotEmpty(t, res) - res, err = ss.Channel().GetSidebarCategoriesForTeamForUser(userId, teamId2) + res, err = ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team2.Id) require.NoError(t, err) require.Len(t, res.Categories, 3) @@ -2053,12 +2146,12 @@ func testClearSidebarOnTeamLeave(t *testing.T, ss store.Store, s SqlStore) { // Do the same on the second team channel2, nErr := ss.Channel().Save(&model.Channel{ Name: model.NewId(), - TeamId: teamId2, + TeamId: team2.Id, Type: model.ChannelTypeOpen, }, 1000) require.NoError(t, nErr) - _, err = ss.Channel().CreateSidebarCategory(userId, teamId2, &model.SidebarCategoryWithChannels{ + _, err = ss.Channel().CreateSidebarCategory(userId, team2.Id, &model.SidebarCategoryWithChannels{ Channels: []string{channel2.Id, dmChannel1.Id}, }) require.NoError(t, err) @@ -2087,7 +2180,7 @@ func testClearSidebarOnTeamLeave(t *testing.T, ss store.Store, s SqlStore) { assert.Equal(t, int64(2), count) // Confirm that the categories on the second team are unchanged - res, err = ss.Channel().GetSidebarCategoriesForTeamForUser(userId, teamId2) + res, err = ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team2.Id) require.NoError(t, err) assert.Len(t, res.Categories, 4) @@ -2267,13 +2360,13 @@ func testUpdateSidebarChannelsByPreferences(t *testing.T, ss store.Store) { // to catch a bug. func testSidebarCategoryDeadlock(t *testing.T, ss store.Store) { userID := model.NewId() - teamID := model.NewId() + team := setupTeam(t, ss, userID) // Join a channel channel, err := ss.Channel().Save(&model.Channel{ Name: "channel", Type: model.ChannelTypeOpen, - TeamId: teamID, + TeamId: team.Id, }, 10) require.NoError(t, err) _, err = ss.Channel().SaveMember(&model.ChannelMember{ @@ -2285,20 +2378,20 @@ func testSidebarCategoryDeadlock(t *testing.T, ss store.Store) { // And then create the initial categories so that it includes the channel opts := &store.SidebarCategorySearchOpts{ - TeamID: teamID, + TeamID: team.Id, ExcludeTeam: false, } res, err := ss.Channel().CreateInitialSidebarCategories(userID, opts) require.NoError(t, err) require.NotEmpty(t, res) - initialCategories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userID, teamID) + initialCategories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userID, team.Id) require.NoError(t, err) channelsCategory := initialCategories.Categories[1] require.Equal(t, []string{channel.Id}, channelsCategory.Channels) - customCategory, err := ss.Channel().CreateSidebarCategory(userID, teamID, &model.SidebarCategoryWithChannels{}) + customCategory, err := ss.Channel().CreateSidebarCategory(userID, team.Id, &model.SidebarCategoryWithChannels{}) require.NoError(t, err) var wg sync.WaitGroup @@ -2306,7 +2399,7 @@ func testSidebarCategoryDeadlock(t *testing.T, ss store.Store) { go func() { defer wg.Done() - _, _, err := ss.Channel().UpdateSidebarCategories(userID, teamID, []*model.SidebarCategoryWithChannels{ + _, _, err := ss.Channel().UpdateSidebarCategories(userID, team.Id, []*model.SidebarCategoryWithChannels{ { SidebarCategory: channelsCategory.SidebarCategory, Channels: []string{}, From 279c448da35b982500f57b0b93c971ad6f5acb3e Mon Sep 17 00:00:00 2001 From: Agniva De Sarker Date: Tue, 11 Oct 2022 19:04:09 +0530 Subject: [PATCH 09/63] MM-36295: Do not convert thumbnails/previews to jpeg for png images (#21230) We were applying a white background to transparent images and converting them to jpegs. This was to make text be legible behind a black preview background. However, this has led to a poor user experience, as users rarely download the full image but always click on previews. Therefore, we need the previews to remain as pngs. To fix this, we just re-encode them as pngs instead of jpgs. ```release-note NONE ``` --- api4/file_test.go | 16 +++-- app/file.go | 102 ++++++++++++++++------------ app/file_test.go | 2 +- app/slack.go | 8 +-- app/upload.go | 4 +- services/slackimport/slackimport.go | 12 ++-- tests/10000x1_expected_preview.jpeg | Bin 1076 -> 0 bytes tests/10000x1_expected_preview.png | Bin 0 -> 92 bytes tests/10000x1_expected_thumb.jpeg | Bin 628 -> 0 bytes tests/10000x1_expected_thumb.png | Bin 0 -> 76 bytes tests/1x10000_expected_preview.jpeg | Bin 3096 -> 0 bytes tests/1x10000_expected_preview.png | Bin 0 -> 121 bytes tests/1x10000_expected_thumb.jpeg | Bin 624 -> 0 bytes tests/1x10000_expected_thumb.png | Bin 0 -> 79 bytes 14 files changed, 83 insertions(+), 61 deletions(-) delete mode 100644 tests/10000x1_expected_preview.jpeg create mode 100644 tests/10000x1_expected_preview.png delete mode 100644 tests/10000x1_expected_thumb.jpeg create mode 100644 tests/10000x1_expected_thumb.png delete mode 100644 tests/1x10000_expected_preview.jpeg create mode 100644 tests/1x10000_expected_preview.png delete mode 100644 tests/1x10000_expected_thumb.jpeg create mode 100644 tests/1x10000_expected_thumb.png diff --git a/api4/file_test.go b/api4/file_test.go index 0184f972b7..85bd04ee21 100644 --- a/api4/file_test.go +++ b/api4/file_test.go @@ -397,8 +397,8 @@ func TestUploadFiles(t *testing.T) { { title: "Happy image thumbnail/preview 10", names: []string{"10000x1.png"}, - expectedImageThumbnailNames: []string{"10000x1_expected_thumb.jpeg"}, - expectedImagePreviewNames: []string{"10000x1_expected_preview.jpeg"}, + expectedImageThumbnailNames: []string{"10000x1_expected_thumb.png"}, + expectedImagePreviewNames: []string{"10000x1_expected_preview.png"}, expectImage: true, expectedImageWidths: []int{10000}, expectedImageHeights: []int{1}, @@ -409,8 +409,8 @@ func TestUploadFiles(t *testing.T) { { title: "Happy image thumbnail/preview 11", names: []string{"1x10000.png"}, - expectedImageThumbnailNames: []string{"1x10000_expected_thumb.jpeg"}, - expectedImagePreviewNames: []string{"1x10000_expected_preview.jpeg"}, + expectedImageThumbnailNames: []string{"1x10000_expected_thumb.png"}, + expectedImagePreviewNames: []string{"1x10000_expected_preview.png"}, expectImage: true, expectedImageWidths: []int{1}, expectedImageHeights: []int{10000}, @@ -678,8 +678,12 @@ func TestUploadFiles(t *testing.T) { fmt.Sprintf("File %v saved to:%q, expected:%q", dbInfo.Name, dbInfo.Path, expectedPath)) if tc.expectImage { - expectedThumbnailPath := fmt.Sprintf("%s/%s_thumb.jpg", expectedDir, name) - expectedPreviewPath := fmt.Sprintf("%s/%s_preview.jpg", expectedDir, name) + // We convert all other image types to jpeg, except pngs. + if ext != ".png" { + ext = ".jpg" + } + expectedThumbnailPath := fmt.Sprintf("%s/%s_thumb%s", expectedDir, name, ext) + expectedPreviewPath := fmt.Sprintf("%s/%s_preview%s", expectedDir, name, ext) assert.Equal(t, dbInfo.ThumbnailPath, expectedThumbnailPath, fmt.Sprintf("Thumbnail for %v saved to:%q, expected:%q", dbInfo.Name, dbInfo.ThumbnailPath, expectedThumbnailPath)) assert.Equal(t, dbInfo.PreviewPath, expectedPreviewPath, diff --git a/app/file.go b/app/file.go index b0e68f4921..5f899aff70 100644 --- a/app/file.go +++ b/app/file.go @@ -266,8 +266,8 @@ func (a *App) getInfoForFilename(post *model.Post, teamID, channelID, userID, ol if info.IsImage() && !info.IsSvg() { nameWithoutExtension := name[:strings.LastIndex(name, ".")] - info.PreviewPath = pathPrefix + nameWithoutExtension + "_preview.jpg" - info.ThumbnailPath = pathPrefix + nameWithoutExtension + "_thumb.jpg" + info.PreviewPath = pathPrefix + nameWithoutExtension + "_preview." + getFileExtFromMimeType(info.MimeType) + info.ThumbnailPath = pathPrefix + nameWithoutExtension + "_thumb." + getFileExtFromMimeType(info.MimeType) } return info @@ -724,8 +724,8 @@ func (t *UploadFileTask) preprocessImage() *model.AppError { t.fileinfo.HasPreviewImage = true nameWithoutExtension := t.Name[:strings.LastIndex(t.Name, ".")] - t.fileinfo.PreviewPath = t.pathPrefix() + nameWithoutExtension + "_preview.jpg" - t.fileinfo.ThumbnailPath = t.pathPrefix() + nameWithoutExtension + "_thumb.jpg" + t.fileinfo.PreviewPath = t.pathPrefix() + nameWithoutExtension + "_preview." + getFileExtFromMimeType(t.fileinfo.MimeType) + t.fileinfo.ThumbnailPath = t.pathPrefix() + nameWithoutExtension + "_thumb." + getFileExtFromMimeType(t.fileinfo.MimeType) // check the image orientation with goexif; consume the bytes we // already have first, then keep Tee-ing from input. @@ -770,20 +770,22 @@ func (t *UploadFileTask) postprocessImage(file io.Reader) { defer release() } - // Fill in the background of a potentially-transparent png file as white - if imgType == "png" { - imaging.FillImageTransparency(decoded, image.White) - } - decoded = imaging.MakeImageUpright(decoded, t.imageOrientation) if decoded == nil { return } - writeJPEG := func(img image.Image, path string) { + writeImage := func(img image.Image, path string) { r, w := io.Pipe() go func() { - err := t.imgEncoder.EncodeJPEG(w, img, jpegEncQuality) + var err error + // It's okay to access imgType in a separate goroutine, + // because imgType is only written once and never written again. + if imgType == "png" { + err = t.imgEncoder.EncodePNG(w, img) + } else { + err = t.imgEncoder.EncodeJPEG(w, img, jpegEncQuality) + } if err != nil { mlog.Error("Unable to encode image as jpeg", mlog.String("path", path), mlog.Err(err)) w.CloseWithError(err) @@ -804,12 +806,12 @@ func (t *UploadFileTask) postprocessImage(file io.Reader) { // This is needed on mobile in case of animated GIFs. go func() { defer wg.Done() - writeJPEG(imaging.GenerateThumbnail(decoded, imageThumbnailWidth, imageThumbnailHeight), t.fileinfo.ThumbnailPath) + writeImage(imaging.GenerateThumbnail(decoded, imageThumbnailWidth, imageThumbnailHeight), t.fileinfo.ThumbnailPath) }() go func() { defer wg.Done() - writeJPEG(imaging.GeneratePreview(decoded, imagePreviewWidth), t.fileinfo.PreviewPath) + writeImage(imaging.GeneratePreview(decoded, imagePreviewWidth), t.fileinfo.PreviewPath) }() go func() { @@ -889,8 +891,8 @@ func (a *App) DoUploadFileExpectModification(c request.CTX, now time.Time, rawTe } nameWithoutExtension := filename[:strings.LastIndex(filename, ".")] - info.PreviewPath = pathPrefix + nameWithoutExtension + "_preview.jpg" - info.ThumbnailPath = pathPrefix + nameWithoutExtension + "_thumb.jpg" + info.PreviewPath = pathPrefix + nameWithoutExtension + "_preview." + getFileExtFromMimeType(info.MimeType) + info.ThumbnailPath = pathPrefix + nameWithoutExtension + "_thumb." + getFileExtFromMimeType(info.MimeType) } if pluginsEnvironment := a.GetPluginsEnvironment(); pluginsEnvironment != nil { @@ -949,40 +951,33 @@ func (a *App) HandleImages(previewPathList []string, thumbnailPathList []string, wg := new(sync.WaitGroup) for i := range fileData { - img, release, err := prepareImage(a.ch.imgDecoder, bytes.NewReader(fileData[i])) + img, imgType, release, err := prepareImage(a.ch.imgDecoder, bytes.NewReader(fileData[i])) if err != nil { mlog.Debug("Failed to prepare image", mlog.Err(err)) continue } wg.Add(2) - go func(img image.Image, path string) { + go func(img image.Image, imgType, path string) { defer wg.Done() - a.generateThumbnailImage(img, path) - }(img, thumbnailPathList[i]) + a.generateThumbnailImage(img, imgType, path) + }(img, imgType, thumbnailPathList[i]) - go func(img image.Image, path string) { + go func(img image.Image, imgType, path string) { defer wg.Done() - a.generatePreviewImage(img, path) - }(img, previewPathList[i]) + a.generatePreviewImage(img, imgType, path) + }(img, imgType, previewPathList[i]) wg.Wait() release() } } -func prepareImage(imgDecoder *imaging.Decoder, imgData io.ReadSeeker) (img image.Image, release func(), err error) { +func prepareImage(imgDecoder *imaging.Decoder, imgData io.ReadSeeker) (img image.Image, imgType string, release func(), err error) { // Decode image bytes into Image object - var imgType string img, imgType, release, err = imgDecoder.DecodeMemBounded(imgData) if err != nil { - return nil, nil, fmt.Errorf("prepareImage: failed to decode image: %w", err) + return nil, "", nil, fmt.Errorf("prepareImage: failed to decode image: %w", err) } - - // Fill in the background of a potentially-transparent png file as white - if imgType == "png" { - imaging.FillImageTransparency(img, image.White) - } - imgData.Seek(0, io.SeekStart) // Flip the image to be upright @@ -992,14 +987,23 @@ func prepareImage(imgDecoder *imaging.Decoder, imgData io.ReadSeeker) (img image } img = imaging.MakeImageUpright(img, orientation) - return img, release, nil + return img, imgType, release, nil } -func (a *App) generateThumbnailImage(img image.Image, thumbnailPath string) { +func (a *App) generateThumbnailImage(img image.Image, imgType, thumbnailPath string) { var buf bytes.Buffer - if err := a.ch.imgEncoder.EncodeJPEG(&buf, imaging.GenerateThumbnail(img, imageThumbnailWidth, imageThumbnailHeight), jpegEncQuality); err != nil { - mlog.Error("Unable to encode image as jpeg", mlog.String("path", thumbnailPath), mlog.Err(err)) - return + + thumb := imaging.GenerateThumbnail(img, imageThumbnailWidth, imageThumbnailHeight) + if imgType == "png" { + if err := a.ch.imgEncoder.EncodePNG(&buf, thumb); err != nil { + mlog.Error("Unable to encode image as png", mlog.String("path", thumbnailPath), mlog.Err(err)) + return + } + } else { + if err := a.ch.imgEncoder.EncodeJPEG(&buf, thumb, jpegEncQuality); err != nil { + mlog.Error("Unable to encode image as jpeg", mlog.String("path", thumbnailPath), mlog.Err(err)) + return + } } if _, err := a.WriteFile(&buf, thumbnailPath); err != nil { @@ -1008,13 +1012,20 @@ func (a *App) generateThumbnailImage(img image.Image, thumbnailPath string) { } } -func (a *App) generatePreviewImage(img image.Image, previewPath string) { +func (a *App) generatePreviewImage(img image.Image, imgType, previewPath string) { var buf bytes.Buffer - preview := imaging.GeneratePreview(img, imagePreviewWidth) - if err := a.ch.imgEncoder.EncodeJPEG(&buf, preview, jpegEncQuality); err != nil { - mlog.Error("Unable to encode image as preview jpg", mlog.Err(err), mlog.String("path", previewPath)) - return + preview := imaging.GeneratePreview(img, imagePreviewWidth) + if imgType == "png" { + if err := a.ch.imgEncoder.EncodePNG(&buf, preview); err != nil { + mlog.Error("Unable to encode image as preview png", mlog.Err(err), mlog.String("path", previewPath)) + return + } + } else { + if err := a.ch.imgEncoder.EncodeJPEG(&buf, preview, jpegEncQuality); err != nil { + mlog.Error("Unable to encode image as preview jpg", mlog.Err(err), mlog.String("path", previewPath)) + return + } } if _, err := a.WriteFile(&buf, previewPath); err != nil { @@ -1033,7 +1044,7 @@ func (a *App) generateMiniPreview(fi *model.FileInfo) { return } defer file.Close() - img, release, err := prepareImage(a.ch.imgDecoder, file) + img, _, release, err := prepareImage(a.ch.imgDecoder, file) if err != nil { mlog.Debug("generateMiniPreview: prepareImage failed", mlog.Err(err), mlog.String("fileinfo_id", fi.Id), mlog.String("channel_id", fi.ChannelId), @@ -1409,3 +1420,10 @@ func (a *App) getCloudFilesSizeLimit() (int64, *model.AppError) { return int64(math.Ceil(float64(*limits.Files.TotalStorage) / 8)), nil } + +func getFileExtFromMimeType(mimeType string) string { + if mimeType == "image/png" { + return "png" + } + return "jpg" +} diff --git a/app/file_test.go b/app/file_test.go index 599c4c6da1..09c03e640e 100644 --- a/app/file_test.go +++ b/app/file_test.go @@ -338,7 +338,7 @@ func TestGenerateThumbnailImage(t *testing.T) { thumbnailPath := filepath.Join(dataPath, thumbnailName) // when - th.App.generateThumbnailImage(img, thumbnailName) + th.App.generateThumbnailImage(img, "jpg", thumbnailName) defer os.Remove(thumbnailPath) // then diff --git a/app/slack.go b/app/slack.go index f7baeb53bc..b108b42980 100644 --- a/app/slack.go +++ b/app/slack.go @@ -40,12 +40,12 @@ func (a *App) SlackImport(c *request.Context, fileData multipart.File, fileSize GeneratePreviewImage: a.generatePreviewImage, InvalidateAllCaches: func() { a.ch.srv.InvalidateAllCaches() }, MaxPostSize: func() int { return a.ch.srv.platform.MaxPostSize() }, - PrepareImage: func(fileData []byte) (image.Image, func(), error) { - img, release, err := prepareImage(a.ch.imgDecoder, bytes.NewReader(fileData)) + PrepareImage: func(fileData []byte) (image.Image, string, func(), error) { + img, imgType, release, err := prepareImage(a.ch.imgDecoder, bytes.NewReader(fileData)) if err != nil { - return nil, nil, err + return nil, "", nil, err } - return img, release, err + return img, imgType, release, err }, } diff --git a/app/upload.go b/app/upload.go index 15ccfba51a..3909f5e096 100644 --- a/app/upload.go +++ b/app/upload.go @@ -298,8 +298,8 @@ func (a *App) UploadData(c *request.Context, us *model.UploadSession, rd io.Read } nameWithoutExtension := info.Name[:strings.LastIndex(info.Name, ".")] - info.PreviewPath = filepath.Dir(info.Path) + "/" + nameWithoutExtension + "_preview.jpg" - info.ThumbnailPath = filepath.Dir(info.Path) + "/" + nameWithoutExtension + "_thumb.jpg" + info.PreviewPath = filepath.Dir(info.Path) + "/" + nameWithoutExtension + "_preview." + getFileExtFromMimeType(info.MimeType) + info.ThumbnailPath = filepath.Dir(info.Path) + "/" + nameWithoutExtension + "_thumb." + getFileExtFromMimeType(info.MimeType) imgData, fileErr := a.ReadFile(uploadPath) if fileErr != nil { return nil, fileErr diff --git a/services/slackimport/slackimport.go b/services/slackimport/slackimport.go index 03863b202a..5b3b15df9f 100644 --- a/services/slackimport/slackimport.go +++ b/services/slackimport/slackimport.go @@ -91,11 +91,11 @@ type Actions struct { CreateGroupChannel func(request.CTX, []string) (*model.Channel, *model.AppError) CreateChannel func(*model.Channel, bool) (*model.Channel, *model.AppError) DoUploadFile func(time.Time, string, string, string, string, []byte) (*model.FileInfo, *model.AppError) - GenerateThumbnailImage func(image.Image, string) - GeneratePreviewImage func(image.Image, string) + GenerateThumbnailImage func(image.Image, string, string) + GeneratePreviewImage func(image.Image, string, string) InvalidateAllCaches func() MaxPostSize func() int - PrepareImage func(fileData []byte) (image.Image, func(), error) + PrepareImage func(fileData []byte) (image.Image, string, func(), error) } // SlackImporter is a service that allows to import slack dumps into mattermost @@ -793,13 +793,13 @@ func (si *SlackImporter) oldImportFile(timestamp time.Time, file io.Reader, team } if fileInfo.IsImage() && !fileInfo.IsSvg() { - img, release, err := si.actions.PrepareImage(data) + img, imgType, release, err := si.actions.PrepareImage(data) if err != nil { return nil, err } defer release() - si.actions.GenerateThumbnailImage(img, fileInfo.ThumbnailPath) - si.actions.GeneratePreviewImage(img, fileInfo.PreviewPath) + si.actions.GenerateThumbnailImage(img, imgType, fileInfo.ThumbnailPath) + si.actions.GeneratePreviewImage(img, imgType, fileInfo.PreviewPath) } return fileInfo, nil diff --git a/tests/10000x1_expected_preview.jpeg b/tests/10000x1_expected_preview.jpeg deleted file mode 100644 index c547e21eefc9f60da050b4f7d8a8e0f352931f70..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1076 zcmex=nP38T>!MAjrYM$lk!rsKme|$jB_n z`2PswA_fLVRz@&jfC5G)p!?X^IXJnv1sIqZnVFebm_e=us;mXdF|Y`-3Mm>ovIz$! zvMUve7&T5@$f4}C@t|nX#SbdRNkvVZTw>x9l2WQ_>Kd9_CZ=ZQ7M51dF0O9w9-dyo zA)#U65s^{JDXD4c8JStdC8cHM6_r)ZEv;?s9i3g1CQq3GGAU*RJ2VdF$b$$4{OPfBE|D`;VW$ z7#Wx$-T{&j4Gc)#w^HS&+zk7m&W?T T{9k=E7IiU<;^7$v|8D{SFo9Ub diff --git a/tests/10000x1_expected_preview.png b/tests/10000x1_expected_preview.png new file mode 100644 index 0000000000000000000000000000000000000000..bf2ca9cc218dc3f05531b743122fc2ae61df969e GIT binary patch literal 92 zcmeAS@N?(olHy`uVBq!ia0y~yU~d4j89A7MEaktG3U(zMMee&9;OZ5 o-`De~PPqLXD8;~#upTJ*|Nm`&Mi!n!HXA@fp00i_>zopr0H~W6vj6}9 literal 0 HcmV?d00001 diff --git a/tests/10000x1_expected_thumb.jpeg b/tests/10000x1_expected_thumb.jpeg deleted file mode 100644 index 90f2925324ff095484083398f9111f8bf51a8324..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 628 zcmex=nP38T>!MAjrYM$WXz|sKme|$jB_n z`2PswA_fLVRz@&jfC5G)p!?X^IXJnv1sIqZnVFebm_e=us;mXdF|Y`-3Mm>ovIz$! zvMUve7&T5@$f4}C@t|nX#SbdRNkvVZTw>x9l2WQ_>Kd9_CZ=ZQ7M51dF0O9w9-dyo zA)#U65s^{JDXD4c8JStdC8cHM6_r)ZEv;?s9i3g1CQq3GGAU*RJ2VdF$b$$4{OPfBE|D`;VW$ z7#Wx$-T{&j4Gc)#w^HS&+zk7m&W?T Q{9k=E7IiV;!vAjq0Em{u=Kufz diff --git a/tests/10000x1_expected_thumb.png b/tests/10000x1_expected_thumb.png new file mode 100644 index 0000000000000000000000000000000000000000..a354c410473a0afaaffa29debb380348e798873c GIT binary patch literal 76 zcmeAS@N?(olHy`uVBq!ia0vp^6+q0$!2~2{Y5(m3Qo^1tjv*Cul75~y;CjKxz{tSx Z|9|clMwS|xYbSux44$rjF6*2UngDQH6OsS` literal 0 HcmV?d00001 diff --git a/tests/1x10000_expected_preview.jpeg b/tests/1x10000_expected_preview.jpeg deleted file mode 100644 index e2511cb26fca3274a9d8617f7c2aab5eccc3adec..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3096 zcmex=nP38T>!MAjqLEz`)4NsKme|$jB_n z`2PswA_fLVRz@&jfC5G)p!?X^IXJnv1sIqZnVFebm_e=us;mXdF|Y`-3Mm>ovIz$! zvMUve7&T5@$f4}C@t|nX#SbdRNkvVZTw>x9l2WQ_>Kd9_CZ=ZQ7M51dF0O9w9-dyo zA)#U65s^{JDXD4c8JStdC8cHM6_r)ZEv;?s9i3g1CQq3GGAU*RJ2VdF$b$$4{OPfBE|D`;VW$ z7#Wx$-T{&j4Gc)#w^HS&+zk7m&W?T o{9k=E7IiU<;?Xb|O#`E8U^ESkrh(BkFq#HN)4nP38T>!MAjrXx!obMPsKme|$jB_n z`2PswA_fLVRz@&jfC5G)p!?X^IXJnv1sIqZnVFebm_e=us;mXdF|Y`-3Mm>ovIz$! zvMUve7&T5@$f4}C@t|nX#SbdRNkvVZTw>x9l2WQ_>Kd9_CZ=ZQ7M51dF0O9w9-dyo zA)#U65s^{JDXD4c8JStdC8cHM6_r)ZEv;?s9i3g1CQq3GGAU*RJ2VdF$b$$4{OPfBE|D`;VW$ z7#Wx$-T{&j4Gc)#w^HS&+zk7m&W?T Q{9k=E7IiUT!~bsr09t{ Date: Tue, 11 Oct 2022 15:02:13 -0400 Subject: [PATCH 10/63] Update en.json (#21373) Automatic Merge --- i18n/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/i18n/en.json b/i18n/en.json index 0d164e18b0..43d7a804fb 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -9473,7 +9473,7 @@ }, { "id": "web.error.unsupported_browser.min_browser_version.chrome", - "translation": "Version 102+" + "translation": "Version 106+" }, { "id": "web.error.unsupported_browser.min_browser_version.edge", From 2080ac08711573265383f854c7fe064ea7fad341 Mon Sep 17 00:00:00 2001 From: jprusch Date: Wed, 12 Oct 2022 15:11:53 +0200 Subject: [PATCH 11/63] Translated using Weblate (German) Currently translated at 100.0% (2386 of 2386 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/de/ Translated using Weblate (German) Currently translated at 100.0% (2385 of 2385 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/de/ Translated using Weblate (German) Currently translated at 100.0% (2385 of 2385 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/de/ Translated using Weblate (German) Currently translated at 100.0% (2384 of 2384 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/de/ Translated using Weblate (German) Currently translated at 100.0% (2382 of 2382 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/de/ --- i18n/de.json | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/i18n/de.json b/i18n/de.json index ae6d93035c..167ae48b99 100644 --- a/i18n/de.json +++ b/i18n/de.json @@ -4801,11 +4801,11 @@ }, { "id": "web.error.unsupported_browser.min_browser_version.edge", - "translation": "Version 44+" + "translation": "Version 95+" }, { "id": "web.error.unsupported_browser.min_browser_version.chrome", - "translation": "Version 100+" + "translation": "Version 102+" }, { "id": "web.error.unsupported_browser.learn_more", @@ -8380,7 +8380,7 @@ }, { "id": "app.notification.body.dm.subTitle", - "translation": "Währen du weg warst, hat dir {{.SenderName}} eine neue Direktnachricht gesendet." + "translation": "Während du weg warst, hat dir {{.SenderName}} eine neue Direktnachricht gesendet." }, { "id": "app.license.generate_renewal_token.no_license", @@ -9537,5 +9537,21 @@ { "id": "app.user.get_badge_count.app_error", "translation": "Wir konnten den Nachrichtenzähler für den Benutzer nicht abfragen." + }, + { + "id": "app.job.error", + "translation": "Fehler bei der Auftragsausführung." + }, + { + "id": "app.last_accessible_file.app_error", + "translation": "Fehler beim Abrufen der letzten zugänglichen Datei" + }, + { + "id": "app.file.cloud.get.app_error", + "translation": "Die Datei kann nicht abgerufen werden, da sie das Limit des Cloud-Plans überschritten hat." + }, + { + "id": "model.group.name.reserved_name.app_error", + "translation": "Gruppenname existiert bereits als reservierter Name" } ] From be3171276927afa14b25de5dd246df73ead0d6c4 Mon Sep 17 00:00:00 2001 From: Tom De Moor Date: Wed, 12 Oct 2022 15:11:53 +0200 Subject: [PATCH 12/63] Translated using Weblate (Dutch) Currently translated at 100.0% (2386 of 2386 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/nl/ Translated using Weblate (Dutch) Currently translated at 100.0% (2382 of 2382 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/nl/ --- i18n/nl.json | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/i18n/nl.json b/i18n/nl.json index 6f883eaa1a..031dca4c09 100644 --- a/i18n/nl.json +++ b/i18n/nl.json @@ -4325,7 +4325,7 @@ }, { "id": "model.post.is_valid.create_at.app_error", - "translation": "Aangemaakt op moet een geldige tijd bevatten" + "translation": "Aangemaakt op moet een geldige tijd bevatten." }, { "id": "model.post.is_valid.file_ids.app_error", @@ -9537,5 +9537,33 @@ { "id": "api.templates.delinquency_90.title", "translation": "Jouw Mattermost-werkruimte werd gedowngraded" + }, + { + "id": "app.job.error", + "translation": "Fout tijdens het uitvoeren van de opdracht." + }, + { + "id": "model.group.name.reserved_name.app_error", + "translation": "groepsnaam bestaat al als een gereserveerde naam" + }, + { + "id": "app.plugin.product_mode.app_error", + "translation": "Plugin {{.Name}} kan niet worden ingeschakeld in de productmodus." + }, + { + "id": "app.last_accessible_file.app_error", + "translation": "Fout bij het ophalen van het laatst toegankelijke bestand" + }, + { + "id": "app.file.cloud.get.app_error", + "translation": "Kan het bestand niet ophalen omdat dit bestand over de limiet van het cloud plan is." + }, + { + "id": "api.team.invite_guests_to_channels.license.error", + "translation": "Jouw licentie ondersteunt geen gastaccounts" + }, + { + "id": "api.team.invite_guests_to_channels.disabled.error", + "translation": "Gastaccounts zijn uitgeschakeld" } ] From a9cc73140d9c63bea49aff22c39c3ed6bd1d62ff Mon Sep 17 00:00:00 2001 From: Kaya Zeren Date: Wed, 12 Oct 2022 15:11:54 +0200 Subject: [PATCH 13/63] Translated using Weblate (Turkish) Currently translated at 100.0% (2386 of 2386 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/tr/ Translated using Weblate (Turkish) Currently translated at 100.0% (2385 of 2385 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/tr/ Translated using Weblate (Turkish) Currently translated at 100.0% (2384 of 2384 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/tr/ Translated using Weblate (Turkish) Currently translated at 100.0% (2382 of 2382 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/tr/ --- i18n/tr.json | 36 ++++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/i18n/tr.json b/i18n/tr.json index 563015338b..bdb2ca0902 100644 --- a/i18n/tr.json +++ b/i18n/tr.json @@ -2725,7 +2725,7 @@ }, { "id": "app.import.validate_team_import_data.name_reserved.error", - "translation": "Takım adında ayrılmış bir sözcük var." + "translation": "Takım adında sistem kullanımına ayrılmış bir sözcük var." }, { "id": "app.import.validate_team_import_data.scheme_invalid.error", @@ -5577,7 +5577,7 @@ }, { "id": "api.ldap_groups.existing_reserved_name_error", - "translation": "aynı adlı bir grup zaten ayrılmış bir ad olarak var" + "translation": "aynı adlı bir grup zaten sistem kullanımına ayrılmış bir ad olarak var" }, { "id": "api.ldap_groups.existing_user_name_error", @@ -5849,11 +5849,11 @@ }, { "id": "web.error.unsupported_browser.min_browser_version.chrome", - "translation": "Sürüm 100+" + "translation": "Sürüm 106+" }, { "id": "web.error.unsupported_browser.min_browser_version.edge", - "translation": "Sürüm 44+" + "translation": "Sürüm 95+" }, { "id": "web.error.unsupported_browser.min_browser_version.firefox", @@ -9537,5 +9537,33 @@ { "id": "app.user.get_badge_count.app_error", "translation": "Kullanıcının nişan sayısını alamadık." + }, + { + "id": "app.job.error", + "translation": "Görev yürütülürken sorun çıktı." + }, + { + "id": "app.last_accessible_file.app_error", + "translation": "Erişilebilen son dosya alınırken sorun çıktı" + }, + { + "id": "app.file.cloud.get.app_error", + "translation": "Cloud tarifesinin sınırlarını aştığından dosya alınamadı." + }, + { + "id": "model.group.name.reserved_name.app_error", + "translation": "aynı adlı bir grup zaten sistem kullanımına ayrılmış bir ad olarak var" + }, + { + "id": "app.plugin.product_mode.app_error", + "translation": "{{.Name}} uygulama eki ürün kipinde etkinleştirilemez." + }, + { + "id": "api.team.invite_guests_to_channels.license.error", + "translation": "Lisansınız konuk hesaplarının kullanılmasını desteklemiyor" + }, + { + "id": "api.team.invite_guests_to_channels.disabled.error", + "translation": "Konuk hesapları devre dışı bırakılmış" } ] From bc6388b93d109f55a0c2a884c1e467b2ffc8061c Mon Sep 17 00:00:00 2001 From: Dennis Cheng Date: Wed, 12 Oct 2022 15:11:54 +0200 Subject: [PATCH 14/63] Translated using Weblate (Chinese (Traditional)) Currently translated at 75.7% (1805 of 2382 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/zh_Hant/ --- i18n/zh-TW.json | 58 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 55 insertions(+), 3 deletions(-) diff --git a/i18n/zh-TW.json b/i18n/zh-TW.json index a24b40c159..0f3fda947e 100644 --- a/i18n/zh-TW.json +++ b/i18n/zh-TW.json @@ -421,7 +421,7 @@ }, { "id": "api.command_channel_header.update_channel.app_error", - "translation": "更新當前頻道時錯誤。" + "translation": "更新當前頻道名稱時錯誤。" }, { "id": "api.command_channel_purpose.channel.app_error", @@ -453,7 +453,7 @@ }, { "id": "api.command_channel_purpose.update_channel.app_error", - "translation": "更新當前頻道時錯誤。" + "translation": "更新當前頻道用途時錯誤。" }, { "id": "api.command_channel_remove.channel.app_error", @@ -619,7 +619,7 @@ }, { "id": "api.command_help.desc", - "translation": "開啟 Mattermost 說明頁面" + "translation": "顯示 Mattermost 說明頁面" }, { "id": "api.command_help.name", @@ -7302,5 +7302,57 @@ { "id": "api.cloud.teams_limit_reached.create", "translation": "無法建立團隊,已達到團隊數量上限" + }, + { + "id": "api.command_custom_status.success", + "translation": "你的狀態已設為 “{{.EmojiName}} {{.StatusMessage}}”。您可以從頻道側邊欄中的狀態彈出視窗更改您的狀態。" + }, + { + "id": "api.command_custom_status.name", + "translation": "狀態" + }, + { + "id": "api.command_custom_status.hint", + "translation": "[:emoji_name:] [status_message] 或清除" + }, + { + "id": "api.command_custom_status.desc", + "translation": "設定或清除您的狀態" + }, + { + "id": "api.command_custom_status.clear.success", + "translation": "狀態已清除。" + }, + { + "id": "api.command_custom_status.clear.app_error", + "translation": "清除狀態錯誤。" + }, + { + "id": "api.command_custom_status.app_error", + "translation": "設定狀態錯誤。" + }, + { + "id": "api.command_channel_purpose.update_channel.max_length", + "translation": "輸入的文本超出字數限制。頻道用途不得超過 {{.MaxLength}} 個字數。" + }, + { + "id": "api.command_channel_header.update_channel.max_length", + "translation": "輸入的文本超出字數限制。頻道標題不得超過 {{.MaxLength}} 個字數。" + }, + { + "id": "api.cloud.delinquency_email.missing_email_to_trigger", + "translation": "缺少發送拖欠電子郵件的必要欄位。" + }, + { + "id": "api.cloud.cws_webhook_event_missing_error", + "translation": "Webhook 事件未處理。事件可能遺失或無效。" + }, + { + "id": "api.channel.create_channel.direct_channel.team_restricted_error", + "translation": "無法在這些使用者間創建直連頻道因為他們不屬於同一個團隊。" + }, + { + "id": "api.admin.saml.failure_reset_authdata_to_email.app_error", + "translation": "無法寄出重設AuthData欄位失敗的電子郵件。" } ] From a0a22fc9da6f417522729e57513ecc403b1481cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=B3th=20Csaba=20//=20Online=20ERP=20Hungary=20Kft?= Date: Wed, 12 Oct 2022 15:11:54 +0200 Subject: [PATCH 15/63] Translated using Weblate (Hungarian) Currently translated at 98.3% (2346 of 2385 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/hu/ Translated using Weblate (Hungarian) Currently translated at 98.2% (2343 of 2385 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/hu/ Translated using Weblate (Hungarian) Currently translated at 98.3% (2342 of 2382 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/hu/ --- i18n/hu.json | 48 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 44 insertions(+), 4 deletions(-) diff --git a/i18n/hu.json b/i18n/hu.json index b10693e9f8..2f7e270a62 100644 --- a/i18n/hu.json +++ b/i18n/hu.json @@ -3125,7 +3125,7 @@ }, { "id": "oauth.gitlab.tos.error", - "translation": "A GitLab Általános Szerződési Feltételei frissültek. Kérjük, látogassa meg a gitlab.com webhelyet, hogy elfogadja őket, majd próbáljon meg újra bejelentkezni a Mattermost-ba." + "translation": "A GitLab Általános Szerződési Feltételei frissültek. Kérjük, látogassa meg a {{.URL}} webhelyet, hogy elfogadja őket, majd próbáljon meg újra bejelentkezni a Mattermost-ba." }, { "id": "model.websocket_client.connect_fail.app_error", @@ -5469,15 +5469,15 @@ }, { "id": "api.templates.payment_failed.title", - "translation": "Sikertelen fizetés" + "translation": "A fizetési művelet nem sikerült" }, { "id": "api.templates.payment_failed.subject", - "translation": "Művelet szükséges: Sikertelen fizetés a Mattermost Cloudra" + "translation": "Művelet szükséges: Sikertelen fizetés a Mattermost {{.Plan}} csomagra" }, { "id": "api.templates.payment_failed.info3", - "translation": "A Mattermost Cloud zavartalan előfizetésének biztosítása érdekében kérjük, vegye fel a kapcsolatot a pénzintézetével a probléma megoldása érdekében, vagy frissítse fizetési adatait. A fizetési adatok frissítése után a Mattermost megpróbálja rendezni a fennmaradó egyenleget." + "translation": "A Mattermost {{.Plan}} zavartalan elérésének biztosítása érdekében kérjük, vegye fel a kapcsolatot a pénzintézetével a probléma megoldása érdekében, vagy frissítse fizetési adatait. A fizetési adatok frissítése után a Mattermost megpróbálja rendezni a fennmaradó egyenleget." }, { "id": "api.templates.payment_failed.info2", @@ -9354,5 +9354,45 @@ { "id": "api.templates.delinquency_30.subject", "translation": "Lépjen most, hogy megtarthassa a Mattermost {{.Plan}} funkcióit" + }, + { + "id": "app.notify_admin.send_notification_post.app_error", + "translation": "Nem sikerült elküldeni az értesítési bejegyzést." + }, + { + "id": "app.notify_admin.save.app_error", + "translation": "Nem sikerült elmenteni az értesítési beállításokat." + }, + { + "id": "app.job.error", + "translation": "Hiba a művelet végrehajtása közben." + }, + { + "id": "app.cloud.get_current_plan_name.app_error", + "translation": "Nem sikerült lekérni a jelenlegi előfizetési csomag nevét" + }, + { + "id": "api.templates.delinquency_30.button", + "translation": "Fizetés frissítése" + }, + { + "id": "api.templates.delinquency_30.bullet.plugins", + "translation": "Aktív bővítmények és integrációk" + }, + { + "id": "api.cloud.delinquency_email.missing_email_to_trigger", + "translation": "Hiányzó kötelező mezők a késedelmes e-mail küldéséhez." + }, + { + "id": "app.file.cloud.get.app_error", + "translation": "Nem sikerült lekérni a fájlt, mivel az meghaladta a felhőcsomag határértékét." + }, + { + "id": "app.user.get_badge_count.app_error", + "translation": "Nem sikerült lekérni a felhasználó jelvény számlálóját." + }, + { + "id": "model.group.name.reserved_name.app_error", + "translation": "csoport név létezik mint lefoglalt név" } ] From a256ff49836fd434d8c8c9011c0ec684e900555a Mon Sep 17 00:00:00 2001 From: Tilto_ Date: Wed, 12 Oct 2022 15:11:55 +0200 Subject: [PATCH 16/63] Translated using Weblate (Korean) Currently translated at 83.3% (1986 of 2384 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/ko/ --- i18n/ko.json | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/i18n/ko.json b/i18n/ko.json index 6efc6c5e61..79e6d396d5 100644 --- a/i18n/ko.json +++ b/i18n/ko.json @@ -619,7 +619,7 @@ }, { "id": "api.command_help.desc", - "translation": "Mattermost 도움말 페이지 열기" + "translation": "Mattermost 도움말 보기" }, { "id": "api.command_help.name", @@ -7934,5 +7934,41 @@ { "id": "api.cloud.teams_limit_reached.create", "translation": "팀 제한에 도달했기 때문에 팀을 만들 수 없습니다" + }, + { + "id": "api.command_marketplace.desc", + "translation": "마켓플레이스 열기" + }, + { + "id": "api.command_help.success", + "translation": "Mattermost는 도구와 팀을 넘나드는 안전한 소통, 협업, 작업 조율을 위한 오픈소스 플랫폼입니다.\nMattermost는 세가지 핵심 도구를 갖고 있습니다:\n\n**채널** - 1:1 및 그룹 채팅을 통해 팀과의 소통을 유지하세요.\n**[플레이북](/playbooks)** - 반복가능한 작업들을 구성하여 구체적이고 예측 가능한 결과를 만드세요.\n**[보드](/boards)** - 팀의 핵심 마일스톤에 도달하기 위해 칸반 보드에서 작업을 만들고 프로젝트를 관리하세요.\n\n[문서 및 가이드 살펴보기]({{.HelpLink}})" + }, + { + "id": "api.cloud.delinquency_email.missing_email_to_trigger", + "translation": "연체 이메일을 보내기 위한 필수 항목이 누락되었습니다." + }, + { + "id": "api.error_set_first_admin_complete_setup", + "translation": "스토어에서 첫 번째 관리자의 설정을 저장하는 동안 오류가 발생했습니다." + }, + { + "id": "api.error_get_first_admin_visit_marketplace_status", + "translation": "스토어에서 첫 번째 관리자의 마켓플레이스 방문 상태를 검색하는 동안 오류가 발생했습니다." + }, + { + "id": "api.error_get_first_admin_complete_setup", + "translation": "스토어에서 첫 번째 관리자 설정을 검색하는 동안 오류가 발생했습니다." + }, + { + "id": "api.command_remote.invite_summary", + "translation": "AES-256으로 암호화된 초대를 비밀번호와 함께 외부 Mattermost 시스템 관리자에게 보냅니다. 초대를 받기 위해 `{{.Command}}` 슬래시 명령어를 사용합니다.\n\n```\n{{.Invitation}}\n```\n\n**보안 연결이 다음을 통해 Mattermost에 액세스할 수 있는지 확인하십시오** {{.SiteURL}}" + }, + { + "id": "api.command_marketplace.unsupported.app_error", + "translation": "마켓플레이스 명령어는 현재 기기에서 지원하지 않습니다." + }, + { + "id": "api.command_marketplace.name", + "translation": "마켓플레이스" } ] From 280859f457e4095389a2eed7373d70ee528cb21f Mon Sep 17 00:00:00 2001 From: master7 Date: Wed, 12 Oct 2022 15:11:55 +0200 Subject: [PATCH 17/63] Translated using Weblate (Polish) Currently translated at 100.0% (2386 of 2386 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/pl/ Translated using Weblate (Polish) Currently translated at 100.0% (2386 of 2386 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/pl/ Translated using Weblate (Polish) Currently translated at 100.0% (2385 of 2385 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/pl/ Translated using Weblate (Polish) Currently translated at 100.0% (2384 of 2384 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/pl/ --- i18n/pl.json | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/i18n/pl.json b/i18n/pl.json index de2e7d8a68..48fe1d9b82 100644 --- a/i18n/pl.json +++ b/i18n/pl.json @@ -4761,11 +4761,11 @@ }, { "id": "web.error.unsupported_browser.min_browser_version.edge", - "translation": "Wersja 44+" + "translation": "Wersja 95+" }, { "id": "web.error.unsupported_browser.min_browser_version.chrome", - "translation": "Wersja 100+" + "translation": "Wersja 106+" }, { "id": "web.error.unsupported_browser.learn_more", @@ -9542,5 +9542,29 @@ { "id": "app.job.error", "translation": "Błąd podczas wykonywania zadania." + }, + { + "id": "app.last_accessible_file.app_error", + "translation": "Błąd pobierania ostatniego dostępnego pliku" + }, + { + "id": "app.file.cloud.get.app_error", + "translation": "Nie można pobrać pliku, ponieważ przekroczony został limit planu chmury." + }, + { + "id": "model.group.name.reserved_name.app_error", + "translation": "nazwa grupy już istnieje jako nazwa zastrzeżona" + }, + { + "id": "app.plugin.product_mode.app_error", + "translation": "Wtyczka {{.Name}} nie może być włączona w trybie produktu." + }, + { + "id": "api.team.invite_guests_to_channels.license.error", + "translation": "Twoja licencja nie wspiera kont gości" + }, + { + "id": "api.team.invite_guests_to_channels.disabled.error", + "translation": "Konta gości są wyłączone" } ] From c3e3d06f661865c717acc281e4eb8edab8fcc293 Mon Sep 17 00:00:00 2001 From: Dmitriy Danilov Date: Wed, 12 Oct 2022 15:11:56 +0200 Subject: [PATCH 18/63] Translated using Weblate (Ukrainian) Currently translated at 70.7% (1687 of 2385 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/uk/ --- i18n/uk.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/i18n/uk.json b/i18n/uk.json index 21f427b84f..4632894522 100644 --- a/i18n/uk.json +++ b/i18n/uk.json @@ -6858,5 +6858,17 @@ { "id": "api.admin.add_certificate.parseform.app_error", "translation": "Помилка синтаксісу багато-форматного запиту" + }, + { + "id": "api.channel.patch_channel_moderations.cache_invalidation.error", + "translation": "Помилка валідації кешу" + }, + { + "id": "api.back_to_app", + "translation": "Повернутися до {{.SiteName}}" + }, + { + "id": "Channels", + "translation": "Канали" } ] From 9018daba01054132e89a016fa223cd80c926330e Mon Sep 17 00:00:00 2001 From: Matthew Williams Date: Wed, 12 Oct 2022 15:11:56 +0200 Subject: [PATCH 19/63] Translated using Weblate (English (Australia)) Currently translated at 100.0% (2385 of 2385 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/en_AU/ --- i18n/en_AU.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/i18n/en_AU.json b/i18n/en_AU.json index e1dc7cb1c0..026157936b 100644 --- a/i18n/en_AU.json +++ b/i18n/en_AU.json @@ -9537,5 +9537,21 @@ { "id": "app.cloud.get_current_plan_name.app_error", "translation": "Unable to get current plan name" + }, + { + "id": "model.group.name.reserved_name.app_error", + "translation": "group name already exists as a reserved name" + }, + { + "id": "app.last_accessible_file.app_error", + "translation": "Error fetching last accessible file" + }, + { + "id": "app.job.error", + "translation": "Error during job execution." + }, + { + "id": "app.file.cloud.get.app_error", + "translation": "Unable to retrieve file: cloud storage limit exceeded." } ] From ef699ff0a286a62d20297b4879e900bad7cb8a69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20V=C3=ADtor=20Cruz=20de=20Souza?= Date: Wed, 12 Oct 2022 15:11:56 +0200 Subject: [PATCH 20/63] Translated using Weblate (Portuguese (Brazil)) Currently translated at 90.6% (2161 of 2385 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/pt_BR/ Translated using Weblate (Portuguese (Brazil)) Currently translated at 90.5% (2160 of 2385 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/pt_BR/ --- i18n/pt-BR.json | 104 ++++++++++++++++++++++++++---------------------- 1 file changed, 56 insertions(+), 48 deletions(-) diff --git a/i18n/pt-BR.json b/i18n/pt-BR.json index 94b78b3e58..1d35e397f0 100644 --- a/i18n/pt-BR.json +++ b/i18n/pt-BR.json @@ -621,7 +621,7 @@ }, { "id": "api.command_help.desc", - "translation": "Abrir a página de ajuda do Mattermost" + "translation": "Exibir descrição de ajuda do Mattermost" }, { "id": "api.command_help.name", @@ -1173,7 +1173,7 @@ }, { "id": "api.marshal_error", - "translation": "marshal error" + "translation": "Falha para marshal." }, { "id": "api.oauth.allow_oauth.redirect_callback.app_error", @@ -1789,7 +1789,7 @@ }, { "id": "api.templates.email_footer", - "translation": "Para alterar suas preferencias de notificação, faça login no site da sua equipe e vá para Configurações de Conta > Notificações." + "translation": "Para alterar suas preferencias de notificação, faça login no site da sua equipe e vá para Configurações > Notificações." }, { "id": "api.templates.email_info1", @@ -1813,7 +1813,7 @@ }, { "id": "api.templates.invite_body.button", - "translation": "Entrar na Equipe" + "translation": "Entrar agora" }, { "id": "api.templates.invite_body.title", @@ -1857,7 +1857,7 @@ }, { "id": "api.templates.post_body.button", - "translation": "Ir Para a Publicação" + "translation": "Responder no Mattermost" }, { "id": "api.templates.reset_body.button", @@ -1865,7 +1865,7 @@ }, { "id": "api.templates.reset_body.title", - "translation": "Você solicitou uma redefinição de senha" + "translation": "Redefina sua senha" }, { "id": "api.templates.reset_subject", @@ -4333,11 +4333,11 @@ }, { "id": "model.post.is_valid.file_ids.app_error", - "translation": "Ids do arquivo inválido. Observe que os uploads estão limitados a cinco arquivos no máximo. Por favor, use publicações adicionais para mais arquivos." + "translation": "Ids do arquivo inválido. Observe que os uploads estão limitados a 10 arquivos no máximo. Por favor, use publicações adicionais para mais arquivos." }, { "id": "model.post.is_valid.filenames.app_error", - "translation": "Nomes de arquivo inválidos" + "translation": "Nomes de arquivo inválidos." }, { "id": "model.post.is_valid.hashtags.app_error", @@ -4573,7 +4573,7 @@ }, { "id": "oauth.gitlab.tos.error", - "translation": "Os Termos de Serviço do GitLab foram atualizados. Por favor vá até gitlab.com para aceitar os termos e depois tente se logar novamente no Mattermost." + "translation": "Os Termos de Serviço do GitLab foram atualizados. Por favor vá até {{.URL}} para aceitar os termos e depois tente se logar novamente no Mattermost." }, { "id": "plugin.api.update_user_status.bad_status", @@ -4745,19 +4745,19 @@ }, { "id": "web.error.unsupported_browser.min_os_version.windows", - "translation": "Windows 7+" + "translation": "Windows 8.1+" }, { "id": "web.error.unsupported_browser.min_os_version.mac", - "translation": "macOS 10.9+" + "translation": "macOS 10.14+" }, { "id": "web.error.unsupported_browser.min_browser_version.safari", - "translation": "Versão 12+" + "translation": "Versão 14.1+" }, { "id": "web.error.unsupported_browser.min_browser_version.firefox", - "translation": "Versão 60+" + "translation": "Versão 91+" }, { "id": "web.error.unsupported_browser.min_browser_version.edge", @@ -4765,7 +4765,7 @@ }, { "id": "web.error.unsupported_browser.min_browser_version.chrome", - "translation": "Versão 61+" + "translation": "Version 100+" }, { "id": "web.error.unsupported_browser.learn_more", @@ -5945,7 +5945,7 @@ }, { "id": "api.push_notifications.session.expired", - "translation": "Sessão expirada: faça login para continuar recebendo notificações. As sessões para {{.siteName}} estão configuradas pelo Administrador do Sistema para expirar a cada {{.daysCount}} dia(s)." + "translation": "Sessão expirada: faça login para continuar recebendo notificações. As sessões para {{.siteName}} estão configuradas pelo Administrador do Sistema para expirar a cada {{.hoursCount}} hour(s)." }, { "id": "api.post.error_get_post_id.pending", @@ -6189,7 +6189,7 @@ }, { "id": "app.preference.permanent_delete_by_user.app_error", - "translation": "Encontramos um erro enquanto excluía as preferências." + "translation": "Encontramos um erro enquanto excluiamos as preferências." }, { "id": "app.preference.get_category.app_error", @@ -6897,7 +6897,7 @@ }, { "id": "model.upload_session.is_valid.user_id.app_error", - "translation": "Valor inválido para user_id." + "translation": "Valor inválido para UserId" }, { "id": "model.upload_session.is_valid.type.app_error", @@ -6905,15 +6905,15 @@ }, { "id": "model.upload_session.is_valid.path.app_error", - "translation": "Valor inválido para path." + "translation": "Valor inválido para path" }, { "id": "model.upload_session.is_valid.id.app_error", - "translation": "Valor inválido para id." + "translation": "Valor inválido para id" }, { "id": "model.upload_session.is_valid.create_at.app_error", - "translation": "Valor inválido para create_at." + "translation": "Valor inválido para Create_At" }, { "id": "app.upload.upload_data.large_image.app_error", @@ -7601,7 +7601,7 @@ }, { "id": "api.templates.copyright", - "translation": "© 2020 Mattermost, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301" + "translation": "© 2021 Mattermost, Inc. 530 Lytton Avenue, Second floor, Palo Alto, CA, 94301" }, { "id": "api.roles.patch_roles.not_allowed_permission.error", @@ -7645,15 +7645,15 @@ }, { "id": "api.templates.payment_failed.title", - "translation": "Pagamento Falhou" + "translation": "O Pagamento não foi bem sucedido" }, { "id": "api.templates.payment_failed.subject", - "translation": "Ação necessária: Falha no pagamento para Mattermost Cloud" + "translation": "Ação necessária: Falha no pagamento para Mattermost {{.Plan}}" }, { "id": "api.templates.payment_failed.info3", - "translation": "Para garantir a assinatura ininterrupta do Mattermost Cloud, entre em contato com sua instituição financeira para corrigir o problema subjacente ou atualize suas informações de pagamento. Assim que as informações de pagamento forem atualizadas, a Mattermost tentará liquidar o saldo pendente." + "translation": "Para garantir a acesso ininterrupto do Mattermost {{.Plan}}, entre em contato com sua instituição financeira para corrigir o problema subjacente ou atualize suas informações de pagamento. Assim que as informações de pagamento forem atualizadas, a Mattermost tentará liquidar o saldo pendente." }, { "id": "api.templates.payment_failed.info2", @@ -8069,7 +8069,7 @@ }, { "id": "api.user.get_authorization_code.endpoint.app_error", - "translation": "Erro ao recuperar o endpoint da Descoberta de Documento." + "translation": "Erro ao recuperar o endpoint do Documento de Descoberta." }, { "id": "api.templates.welcome_body.subTitle2", @@ -8117,11 +8117,11 @@ }, { "id": "api.templates.questions_footer.info", - "translation": "Envie-nos um email a qualquer momento em " + "translation": "Precisa de ajuda ou tem dúvidas? Envie-nos um e-mail em " }, { "id": "api.templates.email_footer_v2", - "translation": "© 2020 Mattermost, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301" + "translation": "© 2022 Mattermost, Inc. 530 Lytton Avenue, Second floor, Palo Alto, CA, 94301" }, { "id": "api.post.search_files.invalid_body.app_error", @@ -8333,7 +8333,7 @@ }, { "id": "api.context.remote_id_invalid.app_error", - "translation": "Não foi possível encontrar o id do cluster remoto {{.RemoteId}}." + "translation": "Não foi possível encontrar o ID de uma conexão segura {{.RemoteId}}." }, { "id": "api.context.json_encoding.app_error", @@ -8357,15 +8357,15 @@ }, { "id": "api.command_share.remote_uninvited", - "translation": "Remoto `{{.RemoteId}}` sem convite." + "translation": "Conexão segura `{{.RemoteId}}` sem convite." }, { "id": "api.command_share.remote_id_invalid.error", - "translation": "O id do cluster remoto é inválido: {{.Error}}" + "translation": "O ID de conexão segura é inválido: {{.Error}}" }, { "id": "api.command_share.remote_id.help", - "translation": "Id de uma instância remota existente. Veja o comando `remote` para adicionar uma instância remota." + "translation": "ID de uma conexão segura existente. Veja o comando `remote` para adicionar uma conexão segura." }, { "id": "api.command_share.permission_required", @@ -8437,7 +8437,7 @@ }, { "id": "api.templates.cloud_welcome_email.signin_sub_info2", - "translation": "para a melhor experiência em PC, Mac, iOS e Android." + "translation": "para a melhor experiência em Windows, Linux, Mac, iOS e Android." }, { "id": "api.templates.cloud_welcome_email.signin_sub_info", @@ -8469,7 +8469,7 @@ }, { "id": "api.templates.cloud_welcome_email.add_apps_sub_info", - "translation": "Otimize seu trabalho com ferramentas como Github, Google Agenda e Chrome. Explore todas as integrações que temos" + "translation": "Otimize seu trabalho com ferramentas como GitHub, Jira e Zoom. Explore todas as integrações que temos em nosso" }, { "id": "api.templates.cloud_welcome_email.add_apps_info", @@ -8481,11 +8481,11 @@ }, { "id": "api.remote_cluster.update_not_unique.app_error", - "translation": "Já existe um cluster remoto com a mesmo url." + "translation": "Já existe uma conexão segura com a mesmo url." }, { "id": "api.remote_cluster.update.app_error", - "translation": "Encontramos um erro ao atualizar o cluster remoto." + "translation": "Encontramos um erro ao atualizar a conexão segura." }, { "id": "api.remote_cluster.service_not_enabled.app_error", @@ -8493,11 +8493,11 @@ }, { "id": "api.remote_cluster.save_not_unique.app_error", - "translation": "O cluster remoto já foi adicionado." + "translation": "A conexão segura já foi adicionada." }, { "id": "api.remote_cluster.save.app_error", - "translation": "Encontramos um erro ao salvar o cluster remoto." + "translation": "Encontramos um erro ao salvar a conexão segura." }, { "id": "api.remote_cluster.invalid_topic.app_error", @@ -8509,11 +8509,11 @@ }, { "id": "api.remote_cluster.get.app_error", - "translation": "Encontramos um erro ao recuperar um cluster remoto." + "translation": "Encontramos um erro ao recuperar uma conexão segura." }, { "id": "api.remote_cluster.delete.app_error", - "translation": "Encontramos um erro ao excluir o cluster remoto." + "translation": "Encontramos um erro ao excluir uma conexão segura." }, { "id": "api.job.unable_to_create_job.incorrect_job_type", @@ -8533,11 +8533,11 @@ }, { "id": "api.context.remote_id_missing.app_error", - "translation": "Falta a id do cluster remoto." + "translation": "Falta a ID de conexão sergura." }, { "id": "api.context.remote_id_mismatch.app_error", - "translation": "Incompatibilidade de id do cluster remoto." + "translation": "Incompatibilidade de ID da conexão segura." }, { "id": "api.context.invitation_expired.error", @@ -8549,11 +8549,11 @@ }, { "id": "api.command_share.uninvite_remote_id.help", - "translation": "Id da instância remota para desconvidar." + "translation": "ID de uma conexão segura para desconvidar." }, { "id": "api.command_share.uninvite_remote.help", - "translation": "Cancela o convite de uma instância remota deste canal compartilhado" + "translation": "Cancela o convite de uma conexão segura deste canal compartilhado" }, { "id": "api.command_share.shared_channel_unavailable", @@ -8569,11 +8569,11 @@ }, { "id": "api.command_share.remote_not_valid", - "translation": "Deve especificar um cluster remoto válido para cancelar o convite" + "translation": "Deve especificar uma ID de conexão segura válida para cancelar o convite" }, { "id": "api.command_share.remote_already_invited", - "translation": "O cluster remoto já foi convidado." + "translation": "A conexão segura já foi convidada." }, { "id": "api.command_share.not_shared_channel_unshare", @@ -8581,15 +8581,15 @@ }, { "id": "api.command_share.no_remote_invited", - "translation": "Nenhum remoto foi convidado para este canal compartilhado." + "translation": "Nenhuma conexão remota foi convidada a este canal." }, { "id": "api.command_share.name", - "translation": "compartilhar" + "translation": "compartilhar-canal" }, { "id": "api.command_share.must_specify_valid_remote", - "translation": "Deve especificar um ID de conjunto remoto válido para convidar." + "translation": "Deve especificar um ID de conjunto válido seguro para convidar." }, { "id": "api.command_share.missing_action", @@ -8650,5 +8650,13 @@ { "id": "Boards", "translation": "Quadros" + }, + { + "id": "Playbooks", + "translation": "Playbooks" + }, + { + "id": "api.cloud.delinquency_email.missing_email_to_trigger", + "translation": "Campos faltando para envio de email." } ] From 4eb4316402a4a96daec6ded7a321bc23eab31fed Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Wed, 12 Oct 2022 15:11:57 +0200 Subject: [PATCH 21/63] Update translation files Updated by "Cleanup translation files" hook in Weblate. Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/ Update translation files Updated by "Cleanup translation files" hook in Weblate. Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/ --- i18n/bg.json | 8 -------- i18n/de.json | 8 -------- i18n/en_AU.json | 8 -------- i18n/es.json | 8 -------- i18n/fa.json | 8 -------- i18n/fr.json | 8 -------- i18n/hu.json | 8 -------- i18n/it.json | 8 -------- i18n/ja.json | 8 -------- i18n/ko.json | 8 -------- i18n/nl.json | 8 -------- i18n/pl.json | 8 -------- i18n/pt-BR.json | 8 -------- i18n/ro.json | 8 -------- i18n/ru.json | 8 -------- i18n/sv.json | 4 ---- i18n/tr.json | 8 -------- i18n/uk.json | 8 -------- i18n/zh-CN.json | 8 -------- i18n/zh-TW.json | 8 -------- 20 files changed, 156 deletions(-) diff --git a/i18n/bg.json b/i18n/bg.json index 7913a2775f..4031bf4bf6 100644 --- a/i18n/bg.json +++ b/i18n/bg.json @@ -4267,14 +4267,6 @@ "id": "api.team.invite_guests.channel_in_invalid_team.app_error", "translation": "Каналите в поканата трябва да са част от канещият екип." }, - { - "id": "api.team.invate_guests_to_channels.license.error", - "translation": "Вашият лиценз не поддържа профили за гости" - }, - { - "id": "api.team.invate_guests_to_channels.disabled.error", - "translation": "Профилите за гости са забранени" - }, { "id": "api.team.invalidate_all_email_invites.app_error", "translation": "Грешка при анулиране на поканите по ел.поща." diff --git a/i18n/de.json b/i18n/de.json index 167ae48b99..dd3742e5f8 100644 --- a/i18n/de.json +++ b/i18n/de.json @@ -5623,14 +5623,6 @@ "id": "api.team.invite_guests.channel_in_invalid_team.app_error", "translation": "Die Kanäle der Einladung müssen Teil des Teams der Einladung sein." }, - { - "id": "api.team.invate_guests_to_channels.license.error", - "translation": "Deine Lizenz unterstützt Gastkonten nicht" - }, - { - "id": "api.team.invate_guests_to_channels.disabled.error", - "translation": "Gastkonten sind deaktiviert" - }, { "id": "api.team.invalidate_all_email_invites.app_error", "translation": "Fehler beim Annulieren von E-Mail-Einladungen." diff --git a/i18n/en_AU.json b/i18n/en_AU.json index 026157936b..6f6f177f60 100644 --- a/i18n/en_AU.json +++ b/i18n/en_AU.json @@ -5459,14 +5459,6 @@ "id": "api.team.invite_guests.channel_in_invalid_team.app_error", "translation": "The channels of the invite must be part of the team of the invite." }, - { - "id": "api.team.invate_guests_to_channels.license.error", - "translation": "Your licence does not support guest accounts" - }, - { - "id": "api.team.invate_guests_to_channels.disabled.error", - "translation": "Guest accounts are disabled" - }, { "id": "api.team.invalidate_all_email_invites.app_error", "translation": "Error invalidating email invites." diff --git a/i18n/es.json b/i18n/es.json index 97cec02eba..5a29542a5c 100644 --- a/i18n/es.json +++ b/i18n/es.json @@ -5543,14 +5543,6 @@ "id": "api.team.invite_guests.channel_in_invalid_team.app_error", "translation": "Los canales de la invitación deben ser parte del equipo al cual fue invitado." }, - { - "id": "api.team.invate_guests_to_channels.license.error", - "translation": "La licencia actual no es compatible con cuentas de huéspedes" - }, - { - "id": "api.team.invate_guests_to_channels.disabled.error", - "translation": "Las cuentas de huéspedes están deshabilitadas" - }, { "id": "api.team.invalidate_all_email_invites.app_error", "translation": "Error al invalidar las invitaciones por correo electrónico." diff --git a/i18n/fa.json b/i18n/fa.json index b71235d64b..88186a1d34 100644 --- a/i18n/fa.json +++ b/i18n/fa.json @@ -5159,14 +5159,6 @@ "id": "api.team.invite_guests.channel_in_invalid_team.app_error", "translation": "کانال های دعوت باید بخشی از تیم دعوت باشند." }, - { - "id": "api.team.invate_guests_to_channels.license.error", - "translation": "مجوز شما از حساب های مهمان پشتیبانی نمی کند" - }, - { - "id": "api.team.invate_guests_to_channels.disabled.error", - "translation": "حساب های مهمان غیرفعال شده است" - }, { "id": "api.team.invalidate_all_email_invites.app_error", "translation": "خطا در عدم اعتبار دعوت نامه های ایمیل." diff --git a/i18n/fr.json b/i18n/fr.json index 043cda6743..c94978c9da 100644 --- a/i18n/fr.json +++ b/i18n/fr.json @@ -5583,14 +5583,6 @@ "id": "api.team.invite_guests.channel_in_invalid_team.app_error", "translation": "Les canaux présents dans l'invitation doivent faire partie de la même équipe pour laquelle vous êtes invité." }, - { - "id": "api.team.invate_guests_to_channels.license.error", - "translation": "Votre licence ne prend pas en charge les comptes invités." - }, - { - "id": "api.team.invate_guests_to_channels.disabled.error", - "translation": "Les comptes invités sont désactivés" - }, { "id": "api.team.invalidate_all_email_invites.app_error", "translation": "Une erreur s'est produite lors de l'invalidation des e-mails d'invitation." diff --git a/i18n/hu.json b/i18n/hu.json index 2f7e270a62..930d7fd5d3 100644 --- a/i18n/hu.json +++ b/i18n/hu.json @@ -5795,14 +5795,6 @@ "id": "api.team.invite_guests.channel_in_invalid_team.app_error", "translation": "A meghívás csatornáinak a meghívó csapat részének kell lenniük." }, - { - "id": "api.team.invate_guests_to_channels.license.error", - "translation": "Az Ön licence nem támogatja a vendég fiókokat" - }, - { - "id": "api.team.invate_guests_to_channels.disabled.error", - "translation": "A vendég fiókok le vannak tiltva" - }, { "id": "api.team.invalidate_all_email_invites.app_error", "translation": "Hiba történt az e-mail meghívók érvénytelenítésekor." diff --git a/i18n/it.json b/i18n/it.json index 2b4114bad9..f9c6ae0911 100644 --- a/i18n/it.json +++ b/i18n/it.json @@ -5211,10 +5211,6 @@ "id": "api.team.add_members.user_denied", "translation": "Questa squadra è gestita dai gruppi. Questo utente non appartiene a un gruppo sincronizzato con questa squadra." }, - { - "id": "api.team.invate_guests_to_channels.disabled.error", - "translation": "Gli account ospite sono disattivati" - }, { "id": "api.team.remove_member.group_constrained.app_error", "translation": "Impossibile rimuovere un utente da una squadra con vincoli di gruppo." @@ -5643,10 +5639,6 @@ "id": "api.team.invalidate_all_email_invites.app_error", "translation": "Errore invalidando gli inviti email." }, - { - "id": "api.team.invate_guests_to_channels.license.error", - "translation": "La tua licenza non supporta gli account ospite" - }, { "id": "api.team.invite_guests.channel_in_invalid_team.app_error", "translation": "I canali dell'invito devono far parte della squadra dell'invito." diff --git a/i18n/ja.json b/i18n/ja.json index 5918ca411b..2b9e6beead 100644 --- a/i18n/ja.json +++ b/i18n/ja.json @@ -5579,14 +5579,6 @@ "id": "api.team.invite_guests.channel_in_invalid_team.app_error", "translation": "招待チャンネルは招待チーム内のチャンネルでなくてはなりません。" }, - { - "id": "api.team.invate_guests_to_channels.license.error", - "translation": "現在のライセンスではゲストアカウントはサポートされていません" - }, - { - "id": "api.team.invate_guests_to_channels.disabled.error", - "translation": "ゲストアカウントは無効化されています" - }, { "id": "api.team.invalidate_all_email_invites.app_error", "translation": "電子メール招待の無効化でエラーが発生しました。" diff --git a/i18n/ko.json b/i18n/ko.json index 79e6d396d5..7d4b36ccb3 100644 --- a/i18n/ko.json +++ b/i18n/ko.json @@ -5575,14 +5575,6 @@ "id": "api.team.invite_guests.channel_in_invalid_team.app_error", "translation": "초대 채널은 초대한 팀의 채널이어야 합니다." }, - { - "id": "api.team.invate_guests_to_channels.license.error", - "translation": "게스트 계정을 지원하지 않는 라이선스입니다" - }, - { - "id": "api.team.invate_guests_to_channels.disabled.error", - "translation": "게스트 계정이 비활성화되어 있습니다" - }, { "id": "api.team.invalidate_all_email_invites.app_error", "translation": "전자우편 초대에서 유효하지 않는 오류가 발생했습니다." diff --git a/i18n/nl.json b/i18n/nl.json index 031dca4c09..b3c4ec8a45 100644 --- a/i18n/nl.json +++ b/i18n/nl.json @@ -5607,14 +5607,6 @@ "id": "api.team.invite_guests.channel_in_invalid_team.app_error", "translation": "De kanalen van de uitgenodigde moet een kanaal dat deel uitmaakt van het team." }, - { - "id": "api.team.invate_guests_to_channels.license.error", - "translation": "Je licentie ondersteunt geen gastgebruikers" - }, - { - "id": "api.team.invate_guests_to_channels.disabled.error", - "translation": "Gast accounts zijn uitgeschakeld" - }, { "id": "api.team.invalidate_all_email_invites.app_error", "translation": "Fout bij afkeuren e-mailuitnodigingen." diff --git a/i18n/pl.json b/i18n/pl.json index 48fe1d9b82..f62b244a3d 100644 --- a/i18n/pl.json +++ b/i18n/pl.json @@ -5583,14 +5583,6 @@ "id": "api.team.invite_guests.channel_in_invalid_team.app_error", "translation": "Kanały do których chcesz zaprosić, muszą być częścią zespołu do którego chcesz zaprosić." }, - { - "id": "api.team.invate_guests_to_channels.license.error", - "translation": "Twoja licencja nie obsługuje kont gości" - }, - { - "id": "api.team.invate_guests_to_channels.disabled.error", - "translation": "Konta gości są wyłączone" - }, { "id": "api.team.invalidate_all_email_invites.app_error", "translation": "Błąd unieważniania zaproszenia E-Mail." diff --git a/i18n/pt-BR.json b/i18n/pt-BR.json index 1d35e397f0..2873fe7a1d 100644 --- a/i18n/pt-BR.json +++ b/i18n/pt-BR.json @@ -5587,14 +5587,6 @@ "id": "api.team.invite_guests.channel_in_invalid_team.app_error", "translation": "Os canais do convite devem ser parte da equipe do convite." }, - { - "id": "api.team.invate_guests_to_channels.license.error", - "translation": "Sua licença não suporta contas convidado" - }, - { - "id": "api.team.invate_guests_to_channels.disabled.error", - "translation": "Contas convidado estão desativadas" - }, { "id": "api.team.invalidate_all_email_invites.app_error", "translation": "Erro ao invalidar convites por email." diff --git a/i18n/ro.json b/i18n/ro.json index c51b4666e0..6c9fe29085 100644 --- a/i18n/ro.json +++ b/i18n/ro.json @@ -5587,14 +5587,6 @@ "id": "api.team.invite_guests.channel_in_invalid_team.app_error", "translation": "Canalele invitației trebuie să facă parte din echipa invitației." }, - { - "id": "api.team.invate_guests_to_channels.license.error", - "translation": "Licența dvs. nu acceptă conturile de invitați" - }, - { - "id": "api.team.invate_guests_to_channels.disabled.error", - "translation": "Conturile oaspeților sunt dezactivate" - }, { "id": "api.team.invalidate_all_email_invites.app_error", "translation": "Eroare la invalidarea invitațiilor de e-mail." diff --git a/i18n/ru.json b/i18n/ru.json index 3d371f6a36..0b4fd7d438 100644 --- a/i18n/ru.json +++ b/i18n/ru.json @@ -5235,10 +5235,6 @@ "id": "api.team.demote_user_to_guest.license.error", "translation": "Ваша лицензия не поддерживает гостевые учётные записи" }, - { - "id": "api.team.invate_guests_to_channels.license.error", - "translation": "Ваша лицензия не поддерживает гостевые учётные записи" - }, { "id": "api.team.remove_member.group_constrained.app_error", "translation": "Невозможно удалить пользователя из управляемой группами команды." @@ -5675,10 +5671,6 @@ "id": "api.team.invalidate_all_email_invites.app_error", "translation": "Ошибка аннулирования e-mail приглашений." }, - { - "id": "api.team.invate_guests_to_channels.disabled.error", - "translation": "Гостевые учётные записи отключены" - }, { "id": "api.team.invite_guests.channel_in_invalid_team.app_error", "translation": "Каналы в приглашении должны быть частью команды в приглашении." diff --git a/i18n/sv.json b/i18n/sv.json index 2374ce62ad..82bd8030ec 100644 --- a/i18n/sv.json +++ b/i18n/sv.json @@ -5275,10 +5275,6 @@ "id": "api.team.invite_guests.channel_in_invalid_team.app_error", "translation": "De kanaler som ingår i inbjudan måste tillhöra de team som inbjudan gäller för." }, - { - "id": "api.team.invate_guests_to_channels.license.error", - "translation": "Din licens tillåter inte gäståtkomst" - }, { "id": "api.team.invate_guests_to_channels.disabled.error", "translation": "Gäståtkomst har inaktiverats" diff --git a/i18n/tr.json b/i18n/tr.json index bdb2ca0902..cbe04ebdb2 100644 --- a/i18n/tr.json +++ b/i18n/tr.json @@ -5655,14 +5655,6 @@ "id": "api.team.invalidate_all_email_invites.app_error", "translation": "E-posta çağrıları geçersiz kılınırken sorun çıktı." }, - { - "id": "api.team.invate_guests_to_channels.disabled.error", - "translation": "Konuk hesapları kullanılmıyor" - }, - { - "id": "api.team.invate_guests_to_channels.license.error", - "translation": "Lisansınızda konuk hesaplarını kullanma özelliği yok" - }, { "id": "api.team.invite_guests.channel_in_invalid_team.app_error", "translation": "Çağrılan kanallar çağrı yapılan takımın bir parçası olmalıdır." diff --git a/i18n/uk.json b/i18n/uk.json index 4632894522..c05c7895b3 100644 --- a/i18n/uk.json +++ b/i18n/uk.json @@ -5583,14 +5583,6 @@ "id": "api.team.invite_guests.channel_in_invalid_team.app_error", "translation": "The channels of the invite must be part of the team of the invite." }, - { - "id": "api.team.invate_guests_to_channels.license.error", - "translation": "Редакція не підтримує Elasticsearch. " - }, - { - "id": "api.team.invate_guests_to_channels.disabled.error", - "translation": "Guest accounts are disabled" - }, { "id": "api.team.invalidate_all_email_invites.app_error", "translation": "Error invalidating email invites." diff --git a/i18n/zh-CN.json b/i18n/zh-CN.json index 4dba1375ff..cecf6c9252 100644 --- a/i18n/zh-CN.json +++ b/i18n/zh-CN.json @@ -5611,10 +5611,6 @@ "id": "api.team.invalidate_all_email_invites.app_error", "translation": "作废邮件邀请错误。" }, - { - "id": "api.team.invate_guests_to_channels.license.error", - "translation": "您的许可证不支持访客帐号" - }, { "id": "api.team.remove_member.group_constrained.app_error", "translation": "无法从组受限团队移除用户。" @@ -5627,10 +5623,6 @@ "id": "api.team.search_teams.pagination_not_implemented.public_team_search", "translation": "仅搜索公共团队不支持分页。" }, - { - "id": "api.team.invate_guests_to_channels.disabled.error", - "translation": "访客帐号已停用" - }, { "id": "api.templates.remove_expired_license.subject", "translation": "Mattermost 企业许可证已停用。" diff --git a/i18n/zh-TW.json b/i18n/zh-TW.json index 0f3fda947e..11b8fcb9ef 100644 --- a/i18n/zh-TW.json +++ b/i18n/zh-TW.json @@ -5579,14 +5579,6 @@ "id": "api.team.invite_guests.channel_in_invalid_team.app_error", "translation": "邀請的頻道必須屬於邀請的團隊" }, - { - "id": "api.team.invate_guests_to_channels.license.error", - "translation": "授權不支援訪客帳號" - }, - { - "id": "api.team.invate_guests_to_channels.disabled.error", - "translation": "訪客帳號已停用" - }, { "id": "api.team.invalidate_all_email_invites.app_error", "translation": "使電子郵件邀請無效時失敗。" From 1d814f27ae6400249f8b6a477dec9da9449ceb17 Mon Sep 17 00:00:00 2001 From: MArtin Johnson Date: Wed, 12 Oct 2022 15:11:58 +0200 Subject: [PATCH 22/63] Translated using Weblate (Swedish) Currently translated at 100.0% (2385 of 2385 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/sv/ --- i18n/sv.json | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/i18n/sv.json b/i18n/sv.json index 82bd8030ec..290ff4f7e5 100644 --- a/i18n/sv.json +++ b/i18n/sv.json @@ -5275,10 +5275,6 @@ "id": "api.team.invite_guests.channel_in_invalid_team.app_error", "translation": "De kanaler som ingår i inbjudan måste tillhöra de team som inbjudan gäller för." }, - { - "id": "api.team.invate_guests_to_channels.disabled.error", - "translation": "Gäståtkomst har inaktiverats" - }, { "id": "api.team.invalidate_all_email_invites.app_error", "translation": "Fel vid ogiltiggörande av e-postinbjudan." @@ -9537,5 +9533,25 @@ { "id": "app.job.error", "translation": "Fel under utförandet av jobbet." + }, + { + "id": "model.group.name.reserved_name.app_error", + "translation": "gruppnamnet finns redan som ett reserverat namn" + }, + { + "id": "app.last_accessible_file.app_error", + "translation": "Fel vid hämtning av den senast tillgängliga filen" + }, + { + "id": "app.file.cloud.get.app_error", + "translation": "Ditt abonnemang begränsar möjligheten att hämta filen." + }, + { + "id": "api.team.invite_guests_to_channels.license.error", + "translation": "Din licens tillåter inte gäståtkomst" + }, + { + "id": "api.team.invite_guests_to_channels.disabled.error", + "translation": "Gäståtkomst har inaktiverats" } ] From 188a87315ca6725f65450e46cfe7ec6e69fba084 Mon Sep 17 00:00:00 2001 From: JtheBAB Date: Wed, 12 Oct 2022 15:11:58 +0200 Subject: [PATCH 23/63] Translated using Weblate (German) Currently translated at 100.0% (2386 of 2386 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/de/ Translated using Weblate (German) Currently translated at 100.0% (2386 of 2386 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/de/ --- i18n/de.json | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/i18n/de.json b/i18n/de.json index dd3742e5f8..50de9de46d 100644 --- a/i18n/de.json +++ b/i18n/de.json @@ -4805,7 +4805,7 @@ }, { "id": "web.error.unsupported_browser.min_browser_version.chrome", - "translation": "Version 102+" + "translation": "Version 106+" }, { "id": "web.error.unsupported_browser.learn_more", @@ -9545,5 +9545,17 @@ { "id": "model.group.name.reserved_name.app_error", "translation": "Gruppenname existiert bereits als reservierter Name" + }, + { + "id": "app.plugin.product_mode.app_error", + "translation": "Plugin {{.Name}} kann im Produktmodus nicht aktiviert werden." + }, + { + "id": "api.team.invite_guests_to_channels.license.error", + "translation": "Deine Lizenz unterstützt Gastkonten nicht" + }, + { + "id": "api.team.invite_guests_to_channels.disabled.error", + "translation": "Gastkonten sind deaktiviert" } ] From 66b7c45e697d661c342fb6719aad5ebfc67ab13f Mon Sep 17 00:00:00 2001 From: Rob Bierbooms Date: Wed, 12 Oct 2022 15:19:00 +0200 Subject: [PATCH 24/63] [MM-47367] Make sure mini preview is saved to database when updating database (#21304) --- store/sqlstore/file_info_store.go | 1 + store/storetest/file_info_store.go | 34 ++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/store/sqlstore/file_info_store.go b/store/sqlstore/file_info_store.go index dbf47020e2..47d847b9d1 100644 --- a/store/sqlstore/file_info_store.go +++ b/store/sqlstore/file_info_store.go @@ -181,6 +181,7 @@ func (fs SqlFileInfoStore) Upsert(info *model.FileInfo) (*model.FileInfo, error) "Width": info.Width, "Height": info.Height, "HasPreviewImage": info.HasPreviewImage, + "MiniPreview": info.MiniPreview, "Content": info.Content, "RemoteId": info.RemoteId, }). diff --git a/store/storetest/file_info_store.go b/store/storetest/file_info_store.go index 1dc72b0536..4df67ae24a 100644 --- a/store/storetest/file_info_store.go +++ b/store/storetest/file_info_store.go @@ -28,6 +28,7 @@ func TestFileInfoStore(t *testing.T, ss store.Store) { t.Run("FileInfoPermanentDelete", func(t *testing.T) { testFileInfoPermanentDelete(t, ss) }) t.Run("FileInfoPermanentDeleteBatch", func(t *testing.T) { testFileInfoPermanentDeleteBatch(t, ss) }) t.Run("FileInfoPermanentDeleteByUser", func(t *testing.T) { testFileInfoPermanentDeleteByUser(t, ss) }) + t.Run("FileInfoUpdateMinipreview", func(t *testing.T) { testFileInfoUpdateMinipreview(t, ss) }) t.Run("GetFilesBatchForIndexing", func(t *testing.T) { testFileInfoStoreGetFilesBatchForIndexing(t, ss) }) t.Run("CountAll", func(t *testing.T) { testFileInfoStoreCountAll(t, ss) }) t.Run("GetStorageUsage", func(t *testing.T) { testFileInfoGetStorageUsage(t, ss) }) @@ -608,6 +609,39 @@ func testFileInfoPermanentDeleteByUser(t *testing.T, ss store.Store) { require.NoError(t, err) } +func testFileInfoUpdateMinipreview(t *testing.T, ss store.Store) { + info := &model.FileInfo{ + CreatorId: model.NewId(), + Path: "image.png", + } + + info, err := ss.FileInfo().Save(info) + require.NoError(t, err) + require.NotEqual(t, len(info.Id), 0) + + defer func() { + ss.FileInfo().PermanentDelete(info.Id) + }() + + rinfo, err := ss.FileInfo().Get(info.Id) + require.NoError(t, err) + require.Equal(t, info.Id, rinfo.Id) + require.Nil(t, rinfo.MiniPreview) + + miniPreview := []byte{0x0, 0x1, 0x2} + + rinfo.MiniPreview = &miniPreview + + rinfo, err = ss.FileInfo().Upsert(rinfo) + require.NoError(t, err) + require.Equal(t, info.Id, rinfo.Id) + + tinfo, err := ss.FileInfo().Get(info.Id) + require.NoError(t, err) + require.Equal(t, info.Id, tinfo.Id) + require.Equal(t, *tinfo.MiniPreview, miniPreview) +} + func testFileInfoStoreGetFilesBatchForIndexing(t *testing.T, ss store.Store) { c1 := &model.Channel{} c1.TeamId = model.NewId() From 71d1b7df533ecafc4807f7481dafdb13de97a14f Mon Sep 17 00:00:00 2001 From: Ibrahim Serdar Acikgoz Date: Wed, 12 Oct 2022 16:31:16 +0300 Subject: [PATCH 25/63] [MM-47468] Fix store metrics initialization (#21374) * initialize metrics before store * add tests * reflect review comments --- app/platform/cluster.go | 4 +-- app/platform/config.go | 1 - app/platform/enterprise.go | 4 +-- app/platform/helper_test.go | 17 +++++---- app/platform/log.go | 4 +-- app/platform/metrics.go | 30 +++++++--------- app/platform/options.go | 2 +- app/platform/service.go | 32 +++++++++-------- app/platform/service_test.go | 70 ++++++++++++++++++++++++++++++++++++ app/platform/session.go | 6 ++-- app/platform/web_conn.go | 8 ++--- app/platform/web_hub.go | 8 ++--- 12 files changed, 128 insertions(+), 58 deletions(-) diff --git a/app/platform/cluster.go b/app/platform/cluster.go index 47722ba007..6575ab7daa 100644 --- a/app/platform/cluster.go +++ b/app/platform/cluster.go @@ -180,8 +180,8 @@ func (ps *PlatformService) InvokeClusterLeaderChangedListeners() { } func (ps *PlatformService) Publish(message *model.WebSocketEvent) { - if ps.metricsImpl() != nil { - ps.metricsImpl().IncrementWebsocketEvent(message.EventType()) + if ps.metricsIFace != nil { + ps.metricsIFace.IncrementWebsocketEvent(message.EventType()) } ps.PublishSkipClusterSend(message) diff --git a/app/platform/config.go b/app/platform/config.go index 5b0218dcd6..fd5bb05f76 100644 --- a/app/platform/config.go +++ b/app/platform/config.go @@ -32,7 +32,6 @@ type ServiceConfig struct { ConfigStore *config.Store Store store.Store // Optional fields - Metrics einterfaces.MetricsInterface Cluster einterfaces.ClusterInterface } diff --git a/app/platform/enterprise.go b/app/platform/enterprise.go index 37849fb3f3..cf7cdf0399 100644 --- a/app/platform/enterprise.go +++ b/app/platform/enterprise.go @@ -26,8 +26,8 @@ func RegisterLicenseInterface(f func(*PlatformService) einterfaces.LicenseInterf licenseInterface = f } -var metricsInterface func(*PlatformService, string, string) einterfaces.MetricsInterface +var metricsInterfaceFn func(*PlatformService, string, string) einterfaces.MetricsInterface func RegisterMetricsInterface(f func(*PlatformService, string, string) einterfaces.MetricsInterface) { - metricsInterface = f + metricsInterfaceFn = f } diff --git a/app/platform/helper_test.go b/app/platform/helper_test.go index 17e40627a3..b8d2da4594 100644 --- a/app/platform/helper_test.go +++ b/app/platform/helper_test.go @@ -55,7 +55,7 @@ func (ms *mockSuite) UserCanSeeOtherUser(userID string, otherUserId string) (boo return true, nil } -func Setup(tb testing.TB) *TestHelper { +func Setup(tb testing.TB, options ...Option) *TestHelper { if testing.Short() { tb.SkipNow() } @@ -64,7 +64,7 @@ func Setup(tb testing.TB) *TestHelper { dbStore.MarkSystemRanUnitTests() mainHelper.PreloadMigrations() - return setupTestHelper(dbStore, false, true, tb) + return setupTestHelper(dbStore, false, true, tb, options...) } func (th *TestHelper) InitBasic() *TestHelper { @@ -96,9 +96,9 @@ func (th *TestHelper) InitBasic() *TestHelper { return th } -func SetupWithStoreMock(tb testing.TB) *TestHelper { +func SetupWithStoreMock(tb testing.TB, options ...Option) *TestHelper { mockStore := testlib.GetMockStoreForSetupFunctions() - th := setupTestHelper(mockStore, false, false, tb) + th := setupTestHelper(mockStore, false, false, tb, options...) statusMock := mocks.StatusStore{} statusMock.On("UpdateExpiredDNDStatuses").Return([]*model.Status{}, nil) statusMock.On("Get", "user1").Return(&model.Status{UserId: "user1", Status: model.StatusOnline}, nil) @@ -126,7 +126,7 @@ func SetupWithCluster(tb testing.TB, cluster einterfaces.ClusterInterface) *Test return th } -func setupTestHelper(dbStore store.Store, enterprise bool, includeCacheLayer bool, tb testing.TB) *TestHelper { +func setupTestHelper(dbStore store.Store, enterprise bool, includeCacheLayer bool, tb testing.TB, options ...Option) *TestHelper { tempWorkspace, err := ioutil.TempDir("", "apptest") if err != nil { panic(err) @@ -149,7 +149,7 @@ func setupTestHelper(dbStore store.Store, enterprise bool, includeCacheLayer boo ps, err := New(ServiceConfig{ ConfigStore: configStore, Store: dbStore, - }) + }, options...) if err != nil { panic(err) } @@ -181,7 +181,10 @@ func setupTestHelper(dbStore store.Store, enterprise bool, includeCacheLayer boo th.Service.SetLicense(nil) } - th.Service.Start(th.Suite) + err = th.Service.Start(th.Suite) + if err != nil { + panic(err) + } return th } diff --git a/app/platform/log.go b/app/platform/log.go index 1d2cc8cf33..42ad85b54e 100644 --- a/app/platform/log.go +++ b/app/platform/log.go @@ -89,11 +89,11 @@ func (ps *PlatformService) NotificationsLogger() *mlog.Logger { } func (ps *PlatformService) EnableLoggingMetrics() { - if ps.metrics == nil || ps.metricsImpl() == nil { + if ps.metrics == nil || ps.metricsIFace == nil { return } - ps.logger.SetMetricsCollector(ps.metricsImpl().GetLoggerMetricsCollector(), mlog.DefaultMetricsUpdateFreqMillis) + ps.logger.SetMetricsCollector(ps.metricsIFace.GetLoggerMetricsCollector(), mlog.DefaultMetricsUpdateFreqMillis) // logging config needs to be reloaded when metrics collector is added or changed. if err := ps.initLogging(); err != nil { diff --git a/app/platform/metrics.go b/app/platform/metrics.go index f486f85cbc..2253594011 100644 --- a/app/platform/metrics.go +++ b/app/platform/metrics.go @@ -32,20 +32,13 @@ type platformMetrics struct { metricsImpl einterfaces.MetricsInterface - cfgFn func() *model.Config -} - -func (ps *PlatformService) metricsImpl() einterfaces.MetricsInterface { - if ps.metrics == nil { - return nil - } - - return ps.metrics.metricsImpl + cfgFn func() *model.Config + listenAddr string } // resetMetrics resets the metrics server. Clears the metrics if the metrics are disabled by the config. -func (ps *PlatformService) resetMetrics(metricsImpl einterfaces.MetricsInterface, cfgFn func() *model.Config) error { - if !*cfgFn().MetricsSettings.Enable { +func (ps *PlatformService) resetMetrics() error { + if !*ps.Config().MetricsSettings.Enable { if ps.metrics != nil { return ps.metrics.stopMetricsServer() } @@ -59,8 +52,8 @@ func (ps *PlatformService) resetMetrics(metricsImpl einterfaces.MetricsInterface } ps.metrics = &platformMetrics{ - cfgFn: cfgFn, - metricsImpl: metricsImpl, + cfgFn: ps.Config, + metricsImpl: ps.metricsIFace, logger: ps.logger, } @@ -68,8 +61,8 @@ func (ps *PlatformService) resetMetrics(metricsImpl einterfaces.MetricsInterface return err } - if metricsImpl != nil { - metricsImpl.Register() + if ps.metricsIFace != nil { + ps.metricsIFace.Register() } return ps.metrics.startMetricsServer() @@ -122,7 +115,8 @@ func (pm *platformMetrics) startMetricsServer() error { } }() - pm.logger.Info("Metrics and profiling server is started", mlog.String("address", l.Addr().String())) + pm.listenAddr = l.Addr().String() + pm.logger.Info("Metrics and profiling server is started", mlog.String("address", pm.listenAddr)) return nil } @@ -181,7 +175,7 @@ func (ps *PlatformService) HandleMetrics(route string, h http.Handler) { } func (ps *PlatformService) RestartMetrics() error { - return ps.resetMetrics(ps.serviceConfig.Metrics, ps.configStore.Get) + return ps.resetMetrics() } func (ps *PlatformService) Metrics() einterfaces.MetricsInterface { @@ -189,5 +183,5 @@ func (ps *PlatformService) Metrics() einterfaces.MetricsInterface { return nil } - return ps.metricsImpl() + return ps.metricsIFace } diff --git a/app/platform/options.go b/app/platform/options.go index 99d63d8f26..072914e104 100644 --- a/app/platform/options.go +++ b/app/platform/options.go @@ -46,7 +46,7 @@ func StoreOverride(override any) Option { func StoreOverrideWithCache(override store.Store) Option { return func(ps *PlatformService) error { ps.newStore = func() (store.Store, error) { - lcl, err := localcachelayer.NewLocalCacheLayer(override, ps.metricsImpl(), ps.clusterIFace, ps.cacheProvider) + lcl, err := localcachelayer.NewLocalCacheLayer(override, ps.metricsIFace, ps.clusterIFace, ps.cacheProvider) if err != nil { return nil, err } diff --git a/app/platform/service.go b/app/platform/service.go index b5a506ae50..d2104fb2bb 100644 --- a/app/platform/service.go +++ b/app/platform/service.go @@ -39,8 +39,7 @@ type PlatformService struct { WebSocketRouter *WebSocketRouter - serviceConfig *ServiceConfig - configStore *config.Store + configStore *config.Store cacheProvider cache.Provider statusCache cache.Cache @@ -57,6 +56,7 @@ type PlatformService struct { startMetrics bool metrics *platformMetrics + metricsIFace einterfaces.MetricsInterface featureFlagSynchronizerMutex sync.Mutex featureFlagSynchronizer *featureflag.Synchronizer @@ -100,7 +100,6 @@ func New(sc ServiceConfig, options ...Option) (*PlatformService, error) { // Step 0: Create the PlatformService. // ConfigStore is and should be handled on a upper level. ps := &PlatformService{ - serviceConfig: &sc, Store: sc.Store, configStore: sc.ConfigStore, clusterIFace: sc.Cluster, @@ -170,15 +169,20 @@ func New(sc ServiceConfig, options ...Option) (*PlatformService, error) { // Depends on step 3 (s.SearchEngine must be non-nil) ps.initEnterprise() - // Step 5: Store. - // Depends on Step 1 (config), 4 (metrics, cluster) and 5 (cacheProvider). + // Step 5: Init Metrics + if metricsInterfaceFn != nil { + ps.metricsIFace = metricsInterfaceFn(ps, *ps.configStore.Get().SqlSettings.DriverName, *ps.configStore.Get().SqlSettings.DataSource) + } + + // Step 6: Store. + // Depends on Step 0 (config), 1 (cacheProvider), 3 (search engine), 5 (metrics) and cluster. if ps.newStore == nil { ps.newStore = func() (store.Store, error) { - ps.sqlStore = sqlstore.New(ps.Config().SqlSettings, ps.Metrics()) + ps.sqlStore = sqlstore.New(ps.Config().SqlSettings, ps.metricsIFace) lcl, err2 := localcachelayer.NewLocalCacheLayer( retrylayer.New(ps.sqlStore), - ps.Metrics(), + ps.metricsIFace, ps.clusterIFace, ps.cacheProvider, ) @@ -204,7 +208,7 @@ func New(sc ServiceConfig, options ...Option) (*PlatformService, error) { return timerlayer.New( searchStore, - ps.Metrics(), + ps.metricsIFace, ), nil } } @@ -234,20 +238,19 @@ func New(sc ServiceConfig, options ...Option) (*PlatformService, error) { return nil, fmt.Errorf("could not create session cache: %w", err) } + // Step 7: Init License if model.BuildEnterpriseReady == "true" { ps.LoadLicense() } - if metricsInterface != nil { - sc.Metrics = metricsInterface(ps, *ps.configStore.Get().SqlSettings.DriverName, *ps.configStore.Get().SqlSettings.DataSource) - } - + // Step 8: Init Metrics Server depends on step 6 (store) and 7 (license) if ps.startMetrics { - if err = ps.resetMetrics(sc.Metrics, ps.configStore.Get); err != nil { - return nil, err + if mErr := ps.resetMetrics(); mErr != nil { + return nil, mErr } } + // Step 9: Init AsymmetricSigningKey depends on step 6 (store) if err = ps.EnsureAsymmetricSigningKey(); err != nil { return nil, fmt.Errorf("unable to ensure asymmetric signing key: %w", err) } @@ -299,6 +302,7 @@ func (ps *PlatformService) Start(suite SuiteIFace) error { return } }) + ps.licenseListenerId = ps.AddLicenseListener(func(oldLicense, newLicense *model.License) { ps.regenerateClientConfig() diff --git a/app/platform/service_test.go b/app/platform/service_test.go index e53afaae98..b2b7fa1e89 100644 --- a/app/platform/service_test.go +++ b/app/platform/service_test.go @@ -4,12 +4,16 @@ package platform import ( + "net/http" "os" + "strings" "testing" "github.com/mattermost/mattermost-server/v6/config" + "github.com/mattermost/mattermost-server/v6/einterfaces/mocks" "github.com/mattermost/mattermost-server/v6/model" "github.com/mattermost/mattermost-server/v6/store/storetest" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) @@ -83,3 +87,69 @@ func TestReadReplicaDisabledBasedOnLicense(t *testing.T) { require.Len(t, ps.Config().SqlSettings.DataSourceSearchReplicas, 1) }) } + +func TestMetrics(t *testing.T) { + t.Run("ensure the metrics server is not started by default", func(t *testing.T) { + th := Setup(t) + defer th.TearDown() + + require.Nil(t, th.Service.metrics) + }) + + t.Run("ensure the metrics server is started", func(t *testing.T) { + th := Setup(t, StartMetrics()) + defer th.TearDown() + + // there is no config listener for the metrics + // we handle it on config save step + th.Service.UpdateConfig(func(c *model.Config) { + c.MetricsSettings.Enable = model.NewBool(true) + }) + th.Service.SaveConfig(th.Service.Config(), false) + + require.NotNil(t, th.Service.metrics) + metricsAddr := strings.Replace(th.Service.metrics.listenAddr, "[::]", "http://localhost", 1) + + resp, err := http.Get(metricsAddr) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + + th.Service.UpdateConfig(func(c *model.Config) { + c.MetricsSettings.Enable = model.NewBool(false) + }) + th.Service.SaveConfig(th.Service.Config(), false) + + _, err = http.Get(metricsAddr) + require.Error(t, err) + }) + + t.Run("ensure the metrics server is started with advanced metrics", func(t *testing.T) { + th := Setup(t, StartMetrics()) + defer th.TearDown() + + mockMetricsImpl := &mocks.MetricsInterface{} + mockMetricsImpl.On("Register").Return() + + th.Service.metricsIFace = mockMetricsImpl + err := th.Service.resetMetrics() + require.NoError(t, err) + + mockMetricsImpl.AssertExpectations(t) + }) + + t.Run("ensure advanced metrics have database metrics", func(t *testing.T) { + mockMetricsImpl := &mocks.MetricsInterface{} + mockMetricsImpl.On("Register").Return() + mockMetricsImpl.On("ObserveStoreMethodDuration", mock.Anything, mock.Anything, mock.Anything).Return() + + th := Setup(t, StartMetrics(), func(ps *PlatformService) error { + ps.metricsIFace = mockMetricsImpl + return nil + }) + defer th.TearDown() + + _ = th.CreateUserOrGuest(false) + + mockMetricsImpl.AssertExpectations(t) + }) +} diff --git a/app/platform/session.go b/app/platform/session.go index 1b085f7dd1..f6da9aec77 100644 --- a/app/platform/session.go +++ b/app/platform/session.go @@ -59,7 +59,7 @@ func (ps *PlatformService) ClearUserSessionCacheLocal(userID string) { if err := ps.sessionCache.Get(key, &session); err == nil { if session.UserId == userID { ps.sessionCache.Remove(key) - if m := ps.metricsImpl(); m != nil { + if m := ps.metricsIFace; m != nil { m.IncrementMemCacheInvalidationCounterSession() } } @@ -100,11 +100,11 @@ func (ps *PlatformService) ClearAllUsersSessionCache() { func (ps *PlatformService) GetSession(token string) (*model.Session, error) { var session = ps.sessionPool.Get().(*model.Session) if err := ps.sessionCache.Get(token, session); err == nil { - if m := ps.metricsImpl(); m != nil { + if m := ps.metricsIFace; m != nil { m.IncrementMemCacheHitCounterSession() } } else { - if m := ps.metricsImpl(); m != nil { + if m := ps.metricsIFace; m != nil { m.IncrementMemCacheMissCounterSession() } } diff --git a/app/platform/web_conn.go b/app/platform/web_conn.go index 2107333428..4e4fc75062 100644 --- a/app/platform/web_conn.go +++ b/app/platform/web_conn.go @@ -409,7 +409,7 @@ func (wc *WebConn) writePump() { wc.logSocketErr("websocket.drainDeadQueue", err) return } - if m := wc.Platform.metricsImpl(); m != nil { + if m := wc.Platform.metricsIFace; m != nil { m.IncrementWebsocketReconnectEvent(reconnectFound) } } else if wc.hasMsgLoss() { @@ -427,11 +427,11 @@ func (wc *WebConn) writePump() { wc.logSocketErr("websocket.sendHello", err) return } - if m := wc.Platform.metricsImpl(); m != nil { + if m := wc.Platform.metricsIFace; m != nil { m.IncrementWebsocketReconnectEvent(reconnectNotFound) } } else { - if m := wc.Platform.metricsImpl(); m != nil { + if m := wc.Platform.metricsIFace; m != nil { m.IncrementWebsocketReconnectEvent(reconnectLossless) } } @@ -488,7 +488,7 @@ func (wc *WebConn) writePump() { return } - if m := wc.Platform.metricsImpl(); m != nil { + if m := wc.Platform.metricsIFace; m != nil { m.IncrementWebSocketBroadcast(msg.EventType()) } case <-ticker.C: diff --git a/app/platform/web_hub.go b/app/platform/web_hub.go index 3a0d5fd6bb..dc83d574ce 100644 --- a/app/platform/web_hub.go +++ b/app/platform/web_hub.go @@ -142,7 +142,7 @@ func (ps *PlatformService) GetHubForUserId(userID string) *Hub { func (ps *PlatformService) HubRegister(webConn *WebConn) { hub := ps.GetHubForUserId(webConn.UserId) if hub != nil { - if metrics := ps.metricsImpl(); metrics != nil { + if metrics := ps.metricsIFace; metrics != nil { metrics.IncrementWebSocketBroadcastUsersRegistered(strconv.Itoa(hub.connectionIndex), 1) } hub.Register(webConn) @@ -153,7 +153,7 @@ func (ps *PlatformService) HubRegister(webConn *WebConn) { func (ps *PlatformService) HubUnregister(webConn *WebConn) { hub := ps.GetHubForUserId(webConn.UserId) if hub != nil { - if metrics := ps.metricsImpl(); metrics != nil { + if metrics := ps.metricsIFace; metrics != nil { metrics.DecrementWebSocketBroadcastUsersRegistered(strconv.Itoa(hub.connectionIndex), 1) } hub.Unregister(webConn) @@ -317,7 +317,7 @@ func (h *Hub) Broadcast(message *model.WebSocketEvent) { // And possibly, we can look into doing the hub initialization inside // NewServer itself. if h != nil && message != nil { - if metrics := h.platform.metricsImpl(); metrics != nil { + if metrics := h.platform.metricsIFace; metrics != nil { metrics.IncrementWebSocketBroadcastBufferSize(strconv.Itoa(h.connectionIndex), 1) } select { @@ -483,7 +483,7 @@ func (h *Hub) Start(suite SuiteIFace) { connIndex.Remove(directMsg.conn) } case msg := <-h.broadcast: - if metrics := h.platform.metricsImpl(); metrics != nil { + if metrics := h.platform.metricsIFace; metrics != nil { metrics.DecrementWebSocketBroadcastBufferSize(strconv.Itoa(h.connectionIndex), 1) } msg = msg.PrecomputeJSON() From 11665d28ea2cf88e2bfb909eab788478c7cdece5 Mon Sep 17 00:00:00 2001 From: Jesse Hallam Date: Wed, 12 Oct 2022 12:53:38 -0300 Subject: [PATCH 26/63] Pre-package Playbooks v1.32.5 (#21376) * Pre-package Playbooks v1.32.5 * Pre-package Playbooks v1.32.6 --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 80b0d047cd..789655bb01 100644 --- a/Makefile +++ b/Makefile @@ -154,7 +154,7 @@ PLUGIN_PACKAGES += mattermost-plugin-channel-export-v1.0.0 PLUGIN_PACKAGES += mattermost-plugin-custom-attributes-v1.3.0 PLUGIN_PACKAGES += mattermost-plugin-github-v2.0.1 PLUGIN_PACKAGES += mattermost-plugin-gitlab-v1.3.0 -PLUGIN_PACKAGES += mattermost-plugin-playbooks-v1.32.4 +PLUGIN_PACKAGES += mattermost-plugin-playbooks-v1.32.6 PLUGIN_PACKAGES += mattermost-plugin-jenkins-v1.1.0 PLUGIN_PACKAGES += mattermost-plugin-jira-v2.4.0 PLUGIN_PACKAGES += mattermost-plugin-nps-v1.2.0 From a6a44b5824e7bcf0249836b3a0b6d1d423ab24f4 Mon Sep 17 00:00:00 2001 From: Scott Bishel Date: Wed, 12 Oct 2022 11:32:14 -0600 Subject: [PATCH 27/63] prepackage boards v7.4.2 (#21382) Automatic Merge --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 789655bb01..43ccf306a5 100644 --- a/Makefile +++ b/Makefile @@ -160,7 +160,7 @@ PLUGIN_PACKAGES += mattermost-plugin-jira-v2.4.0 PLUGIN_PACKAGES += mattermost-plugin-nps-v1.2.0 PLUGIN_PACKAGES += mattermost-plugin-welcomebot-v1.2.0 PLUGIN_PACKAGES += mattermost-plugin-zoom-v1.6.0 -PLUGIN_PACKAGES += focalboard-v7.4.1 +PLUGIN_PACKAGES += focalboard-v7.4.2 PLUGIN_PACKAGES += mattermost-plugin-apps-v1.1.0 # Prepares the enterprise build if exists. The IGNORE stuff is a hack to get the Makefile to execute the commands outside a target From 1d9fdce9167e46de14fb11983bb263b0bda9870b Mon Sep 17 00:00:00 2001 From: Muhammad Salman Date: Thu, 13 Oct 2022 20:34:48 +0300 Subject: [PATCH 28/63] Revert "add metropolis font to email change verify email html" This reverts commit 9f6497fb0edccbe9d3719fa9cdd999fa1c9295a6. --- templates/email_change_verify_body.html | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/templates/email_change_verify_body.html b/templates/email_change_verify_body.html index 8f6918bfaf..0b171b98ca 100644 --- a/templates/email_change_verify_body.html +++ b/templates/email_change_verify_body.html @@ -2,7 +2,6 @@ - @@ -88,7 +87,7 @@ From 8bd6ec8f994fe5cad8789f220a2aad8a33d0c38a Mon Sep 17 00:00:00 2001 From: Muhammad Salman Date: Thu, 13 Oct 2022 21:00:44 +0300 Subject: [PATCH 29/63] [MM-44790] removing extra margin bottom from the verify email change email heading --- templates/email_change_verify_body.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/email_change_verify_body.html b/templates/email_change_verify_body.html index 0b171b98ca..22a1cec84d 100644 --- a/templates/email_change_verify_body.html +++ b/templates/email_change_verify_body.html @@ -87,7 +87,7 @@ From 3747d998067c50670c2518652efec6ec44b55122 Mon Sep 17 00:00:00 2001 From: Julien Tant <785518+JulienTant@users.noreply.github.com> Date: Thu, 13 Oct 2022 14:23:22 -0700 Subject: [PATCH 30/63] [MM-46694] A/B Test: welcome post (#20926) Co-authored-by: Mattermod --- app/app_iface.go | 1 + app/channel.go | 30 +++++++++++++ app/opentracing/opentracing_layer.go | 17 ++++++++ app/user.go | 13 ++++++ app/user_test.go | 51 ++++++++++++++++++++++ model/feature_flags.go | 4 ++ model/post.go | 2 + store/opentracinglayer/opentracinglayer.go | 18 ++++++++ store/retrylayer/retrylayer.go | 21 +++++++++ store/sqlstore/user_store.go | 10 +++++ store/store.go | 1 + store/storetest/mocks/UserStore.go | 21 +++++++++ store/storetest/user_store.go | 25 +++++++++++ store/timerlayer/timerlayer.go | 16 +++++++ 14 files changed, 230 insertions(+) diff --git a/app/app_iface.go b/app/app_iface.go index ef6b3b4733..aed132ba5f 100644 --- a/app/app_iface.go +++ b/app/app_iface.go @@ -871,6 +871,7 @@ type AppIface interface { InviteNewUsersToTeam(emailList []string, teamID, senderId string) *model.AppError InviteNewUsersToTeamGracefully(memberInvite *model.MemberInvite, teamID, senderId string, reminderInterval string) ([]*model.EmailInviteWithError, *model.AppError) IsCRTEnabledForUser(c request.CTX, userID string) bool + IsFirstAdmin(user *model.User) bool IsFirstUserAccount() bool IsLeader() bool IsPasswordValid(password string) *model.AppError diff --git a/app/channel.go b/app/channel.go index 8da9b638f9..504c7265d6 100644 --- a/app/channel.go +++ b/app/channel.go @@ -12,6 +12,7 @@ import ( "strings" "time" + "github.com/mattermost/logr/v2" "github.com/mattermost/mattermost-server/v6/app/request" "github.com/mattermost/mattermost-server/v6/model" "github.com/mattermost/mattermost-server/v6/plugin" @@ -130,6 +131,35 @@ func (a *App) JoinDefaultChannels(c request.CTX, teamID string, user *model.User message.Add("user_id", user.Id) message.Add("team_id", channel.TeamId) a.Publish(message) + + // A/B Test on the welcome post + if a.Config().FeatureFlags.SendWelcomePost && channelName == model.DefaultChannelName { + nbTeams, err := a.Srv().Store().Team().AnalyticsTeamCount(&model.TeamSearch{ + IncludeDeleted: model.NewBool(true), + }) + if err != nil { + c.Logger().Warn("unable to get number of teams", logr.Err(err)) + return nil + } + + if nbTeams == 1 && a.IsFirstAdmin(user) { + // Post the welcome message + if _, err := a.CreatePost(c, &model.Post{ + ChannelId: channel.Id, + Type: model.PostTypeWelcomePost, + UserId: user.Id, + }, channel, false, false); err != nil { + c.Logger().Warn("unable to post welcome message", logr.Err(err)) + return nil + } + ts := a.Srv().GetTelemetryService() + if ts != nil { + ts.SendTelemetry("welcome-message-sent", map[string]any{ + "category": "growth", + }) + } + } + } } if nErr != nil { diff --git a/app/opentracing/opentracing_layer.go b/app/opentracing/opentracing_layer.go index 6dd849aefb..97f929c77d 100644 --- a/app/opentracing/opentracing_layer.go +++ b/app/opentracing/opentracing_layer.go @@ -11626,6 +11626,23 @@ func (a *OpenTracingAppLayer) IsCRTEnabledForUser(c request.CTX, userID string) return resultVar0 } +func (a *OpenTracingAppLayer) IsFirstAdmin(user *model.User) bool { + origCtx := a.ctx + span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.IsFirstAdmin") + + a.ctx = newCtx + a.app.Srv().Store().SetContext(newCtx) + defer func() { + a.app.Srv().Store().SetContext(origCtx) + a.ctx = origCtx + }() + + defer span.Finish() + resultVar0 := a.app.IsFirstAdmin(user) + + return resultVar0 +} + func (a *OpenTracingAppLayer) IsFirstUserAccount() bool { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.IsFirstUserAccount") diff --git a/app/user.go b/app/user.go index bdb9fc8841..c0a9773e61 100644 --- a/app/user.go +++ b/app/user.go @@ -209,6 +209,19 @@ func (a *App) IsFirstUserAccount() bool { return a.ch.srv.platform.IsFirstUserAccount() } +func (a *App) IsFirstAdmin(user *model.User) bool { + if !user.IsSystemAdmin() { + return false + } + + adminID, err := a.Srv().Store().User().GetFirstSystemAdminID() + if err != nil { + return false + } + + return adminID == user.Id +} + // CreateUser creates a user and sets several fields of the returned User struct to // their zero values. func (a *App) CreateUser(c request.CTX, user *model.User) (*model.User, *model.AppError) { diff --git a/app/user_test.go b/app/user_test.go index 174c193deb..139cc1c1e5 100644 --- a/app/user_test.go +++ b/app/user_test.go @@ -1801,3 +1801,54 @@ func TestCreateUserWithInitialPreferences(t *testing.T) { assert.Equal(t, "false", recommendedNextStepsPref[0].Value) }) } + +func TestIsFirstAdmin(t *testing.T) { + t.Run("should return false if user is not sysadmin", func(t *testing.T) { + th := SetupWithStoreMock(t) + defer th.TearDown() + + Id := model.NewId() + isFirstAdmin := th.App.IsFirstAdmin(&model.User{ + Id: Id, + Roles: model.SystemUserRoleId, + }) + require.False(t, isFirstAdmin) + }) + + t.Run("should return false if user is sysadmin but not the first one", func(t *testing.T) { + th := SetupWithStoreMock(t) + defer th.TearDown() + + Id := model.NewId() + + mockUserStore := storemocks.UserStore{} + mockUserStore.On("GetFirstSystemAdminID").Return(model.NewId(), nil) + + mockStore := th.App.Srv().Store().(*storemocks.Store) + mockStore.On("User").Return(&mockUserStore) + + isFirstAdmin := th.App.IsFirstAdmin(&model.User{ + Id: Id, + Roles: model.SystemAdminRoleId, + }) + require.False(t, isFirstAdmin) + }) + + t.Run("should return true if user is sysadmin and the first one", func(t *testing.T) { + th := SetupWithStoreMock(t) + defer th.TearDown() + + Id := model.NewId() + + mockStore := th.App.Srv().Store().(*storemocks.Store) + mockUserStore := storemocks.UserStore{} + mockUserStore.On("GetFirstSystemAdminID").Return(Id, nil) + mockStore.On("User").Return(&mockUserStore) + + isFirstAdmin := th.App.IsFirstAdmin(&model.User{ + Id: Id, + Roles: model.SystemAdminRoleId, + }) + require.True(t, isFirstAdmin) + }) +} diff --git a/model/feature_flags.go b/model/feature_flags.go index 04b2eab18d..9a111d216b 100644 --- a/model/feature_flags.go +++ b/model/feature_flags.go @@ -72,6 +72,9 @@ type FeatureFlags struct { PlanUpgradeButtonText string + // A/B Test on posting a welcome message + SendWelcomePost bool + PostPriority bool } @@ -99,6 +102,7 @@ func (f *FeatureFlags) SetDefaults() { f.CallsEnabled = true f.BoardsProduct = false f.PlanUpgradeButtonText = "upgrade" + f.SendWelcomePost = true f.PostPriority = false } diff --git a/model/post.go b/model/post.go index 4916779b52..3f9c5a630c 100644 --- a/model/post.go +++ b/model/post.go @@ -44,6 +44,7 @@ const ( PostTypeChannelRestored = "system_channel_restored" PostTypeEphemeral = "system_ephemeral" PostTypeChangeChannelPrivacy = "system_change_chan_privacy" + PostTypeWelcomePost = "system_welcome_post" PostTypeAddBotTeamsChannels = "add_bot_teams_channels" PostTypeSystemWarnMetricStatus = "warn_metric_status" PostTypeMe = "me" @@ -387,6 +388,7 @@ func (o *Post) IsValid(maxPostSize int) *AppError { PostTypeChangeChannelPrivacy, PostTypeAddBotTeamsChannels, PostTypeSystemWarnMetricStatus, + PostTypeWelcomePost, PostTypeMe: default: if !strings.HasPrefix(o.Type, PostCustomTypePrefix) { diff --git a/store/opentracinglayer/opentracinglayer.go b/store/opentracinglayer/opentracinglayer.go index eb65b1f798..44f7d09e86 100644 --- a/store/opentracinglayer/opentracinglayer.go +++ b/store/opentracinglayer/opentracinglayer.go @@ -10801,6 +10801,24 @@ func (s *OpenTracingLayerUserStore) GetEtagForProfilesNotInTeam(teamID string) s return result } +func (s *OpenTracingLayerUserStore) GetFirstSystemAdminID() (string, error) { + origCtx := s.Root.Store.Context() + span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetFirstSystemAdminID") + s.Root.Store.SetContext(newCtx) + defer func() { + s.Root.Store.SetContext(origCtx) + }() + + defer span.Finish() + result, err := s.UserStore.GetFirstSystemAdminID() + if err != nil { + span.LogFields(spanlog.Error(err)) + ext.Error.Set(span, true) + } + + return result, err +} + func (s *OpenTracingLayerUserStore) GetForLogin(loginID string, allowSignInWithUsername bool, allowSignInWithEmail bool) (*model.User, error) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetForLogin") diff --git a/store/retrylayer/retrylayer.go b/store/retrylayer/retrylayer.go index 4efa7172ba..292924476a 100644 --- a/store/retrylayer/retrylayer.go +++ b/store/retrylayer/retrylayer.go @@ -12324,6 +12324,27 @@ func (s *RetryLayerUserStore) GetEtagForProfilesNotInTeam(teamID string) string } +func (s *RetryLayerUserStore) GetFirstSystemAdminID() (string, error) { + + tries := 0 + for { + result, err := s.UserStore.GetFirstSystemAdminID() + if err == nil { + return result, nil + } + if !isRepeatableError(err) { + return result, err + } + tries++ + if tries >= 3 { + err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures") + return result, err + } + timepkg.Sleep(100 * timepkg.Millisecond) + } + +} + func (s *RetryLayerUserStore) GetForLogin(loginID string, allowSignInWithUsername bool, allowSignInWithEmail bool) (*model.User, error) { tries := 0 diff --git a/store/sqlstore/user_store.go b/store/sqlstore/user_store.go index 0f724405d2..bd89779355 100644 --- a/store/sqlstore/user_store.go +++ b/store/sqlstore/user_store.go @@ -1748,6 +1748,16 @@ func (us SqlUserStore) InferSystemInstallDate() (int64, error) { return createAt, nil } +func (us SqlUserStore) GetFirstSystemAdminID() (string, error) { + var id string + err := us.GetReplicaX().Get(&id, "SELECT Id FROM Users WHERE Roles LIKE ? ORDER BY CreateAt ASC LIMIT 1", "%system_admin%") + if err != nil { + return "", errors.Wrap(err, "failed to get first system admin") + } + + return id, nil +} + func (us SqlUserStore) GetUsersBatchForIndexing(startTime int64, startFileID string, limit int) ([]*model.UserForIndexing, error) { users := []*model.User{} usersQuery, args, err := us.usersQuery. diff --git a/store/store.go b/store/store.go index b917adbede..f7c33a938e 100644 --- a/store/store.go +++ b/store/store.go @@ -481,6 +481,7 @@ type UserStore interface { IsEmpty(excludeBots bool) (bool, error) GetUsersWithInvalidEmails(page int, perPage int, restrictedDomains string) ([]*model.User, error) InsertUsers(users []*model.User) error + GetFirstSystemAdminID() (string, error) } type BotStore interface { diff --git a/store/storetest/mocks/UserStore.go b/store/storetest/mocks/UserStore.go index ea0fdcfcc0..290b37dae7 100644 --- a/store/storetest/mocks/UserStore.go +++ b/store/storetest/mocks/UserStore.go @@ -569,6 +569,27 @@ func (_m *UserStore) GetEtagForProfilesNotInTeam(teamID string) string { return r0 } +// GetFirstSystemAdminID provides a mock function with given fields: +func (_m *UserStore) GetFirstSystemAdminID() (string, error) { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + var r1 error + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetForLogin provides a mock function with given fields: loginID, allowSignInWithUsername, allowSignInWithEmail func (_m *UserStore) GetForLogin(loginID string, allowSignInWithUsername bool, allowSignInWithEmail bool) (*model.User, error) { ret := _m.Called(loginID, allowSignInWithUsername, allowSignInWithEmail) diff --git a/store/storetest/user_store.go b/store/storetest/user_store.go index 98d6f2bc4f..6d704854b4 100644 --- a/store/storetest/user_store.go +++ b/store/storetest/user_store.go @@ -94,6 +94,7 @@ func TestUserStore(t *testing.T, ss store.Store, s SqlStore) { t.Run("ResetLastPictureUpdate", func(t *testing.T) { testUserStoreResetLastPictureUpdate(t, ss) }) t.Run("GetKnownUsers", func(t *testing.T) { testGetKnownUsers(t, ss) }) t.Run("GetUsersWithInvalidEmails", func(t *testing.T) { testGetUsersWithInvalidEmails(t, ss) }) + t.Run("GetFirstSystemAdminID", func(t *testing.T) { testUserStoreGetFirstSystemAdminID(t, ss) }) } func testUserStoreSave(t *testing.T, ss store.Store) { @@ -4163,6 +4164,30 @@ func testCount(t *testing.T, ss store.Store) { } } +func testUserStoreGetFirstSystemAdminID(t *testing.T, ss store.Store) { + sysAdmin := &model.User{} + sysAdmin.Email = MakeEmail() + sysAdmin.Roles = model.SystemAdminRoleId + " " + model.SystemUserRoleId + sysAdmin, err := ss.User().Save(sysAdmin) + require.NoError(t, err) + defer func() { require.NoError(t, ss.User().PermanentDelete(sysAdmin.Id)) }() + + // We need the second system admin to be created after the first one + // our granulirity is ms + time.Sleep(1 * time.Millisecond) + + sysAdmin2 := &model.User{} + sysAdmin2.Email = MakeEmail() + sysAdmin2.Roles = model.SystemAdminRoleId + " " + model.SystemUserRoleId + sysAdmin2, err = ss.User().Save(sysAdmin2) + require.NoError(t, err) + defer func() { require.NoError(t, ss.User().PermanentDelete(sysAdmin2.Id)) }() + + returnedId, err := ss.User().GetFirstSystemAdminID() + require.NoError(t, err) + require.Equal(t, sysAdmin.Id, returnedId) +} + func testUserStoreAnalyticsActiveCount(t *testing.T, ss store.Store, s SqlStore) { cleanupStatusStore(t, s) diff --git a/store/timerlayer/timerlayer.go b/store/timerlayer/timerlayer.go index 0d7b338767..0558fa66c1 100644 --- a/store/timerlayer/timerlayer.go +++ b/store/timerlayer/timerlayer.go @@ -9727,6 +9727,22 @@ func (s *TimerLayerUserStore) GetEtagForProfilesNotInTeam(teamID string) string return result } +func (s *TimerLayerUserStore) GetFirstSystemAdminID() (string, error) { + start := time.Now() + + result, err := s.UserStore.GetFirstSystemAdminID() + + elapsed := float64(time.Since(start)) / float64(time.Second) + if s.Root.Metrics != nil { + success := "false" + if err == nil { + success = "true" + } + s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetFirstSystemAdminID", success, elapsed) + } + return result, err +} + func (s *TimerLayerUserStore) GetForLogin(loginID string, allowSignInWithUsername bool, allowSignInWithEmail bool) (*model.User, error) { start := time.Now() From d96516acd92585435ad65f0bbe968b551405b12d Mon Sep 17 00:00:00 2001 From: Mattermod Date: Fri, 14 Oct 2022 02:32:14 +0300 Subject: [PATCH 31/63] Update minor version to 7.5.0 (#21405) Automatic Merge --- model/version.go | 1 + 1 file changed, 1 insertion(+) diff --git a/model/version.go b/model/version.go index be63d87971..4bfc461767 100644 --- a/model/version.go +++ b/model/version.go @@ -13,6 +13,7 @@ import ( // It should be maintained in chronological order with most current // release at the front of the list. var versions = []string{ + "7.5.0", "7.4.0", "7.3.0", "7.2.0", From c57706e6f4d4a932ee64f3fdd6b952ff6c818702 Mon Sep 17 00:00:00 2001 From: Agniva De Sarker Date: Fri, 14 Oct 2022 14:17:43 +0530 Subject: [PATCH 32/63] GraphQL: Remove channel stats from the API (#21377) Channel Stats isn't optimized with dataloaders. And it's a heavy call, therefore clients can mistakenly call this with multiple channels and bring down the server. Since there is no advantage compared to a REST API, let's just remove it and keep things safe. ```release-note NONE ``` --- api4/resolver_channel.go | 30 --------------------------- api4/resolver_channel_test.go | 39 ----------------------------------- api4/schema.graphqls | 8 ------- 3 files changed, 77 deletions(-) diff --git a/api4/resolver_channel.go b/api4/resolver_channel.go index e8f10a8a98..2ad2eca523 100644 --- a/api4/resolver_channel.go +++ b/api4/resolver_channel.go @@ -29,36 +29,6 @@ func (ch *channel) Team(ctx context.Context) (*model.Team, error) { return getGraphQLTeam(ctx, ch.TeamId) } -// match with api4.getChannelStats -func (ch *channel) Stats(ctx context.Context) (*model.ChannelStats, error) { - c, err := getCtx(ctx) - if err != nil { - return nil, err - } - - if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), ch.Id, model.PermissionReadChannel) { - c.SetPermissionError(model.PermissionReadChannel) - return nil, c.Err - } - - memberCount, appErr := c.App.GetChannelMemberCount(c.AppContext, ch.Id) - if appErr != nil { - return nil, appErr - } - - guestCount, appErr := c.App.GetChannelGuestCount(c.AppContext, ch.Id) - if appErr != nil { - return nil, appErr - } - - pinnedPostCount, appErr := c.App.GetChannelPinnedPostCount(c.AppContext, ch.Id) - if appErr != nil { - return nil, appErr - } - - return &model.ChannelStats{ChannelId: ch.Id, MemberCount: memberCount, GuestCount: guestCount, PinnedPostCount: pinnedPostCount}, nil -} - func (ch *channel) Cursor() *string { cursor := string(channelCursorPrefix) + "-" + ch.Id encoded := base64.StdEncoding.EncodeToString([]byte(cursor)) diff --git a/api4/resolver_channel_test.go b/api4/resolver_channel_test.go index 36c7989ec1..d5a55dee16 100644 --- a/api4/resolver_channel_test.go +++ b/api4/resolver_channel_test.go @@ -48,12 +48,6 @@ func TestGraphQLChannels(t *testing.T) { ID string `json:"id"` DisplayName string `json:"displayName"` } `json:"team"` - Stats struct { - ChannelId string `json:"channelId"` - MemberCount float64 `json:"memberCount"` - GuestCount float64 `json:"guestCount"` - PinnedPostCount float64 `json:"pinnedpostCount"` - } `json:"stats"` } `json:"channels"` } @@ -388,39 +382,6 @@ func TestGraphQLChannels(t *testing.T) { require.NoError(t, json.Unmarshal(resp.Data, &q)) assert.Len(t, q.Channels, 5) }) - - t.Run("stats", func(t *testing.T) { - query := `query channels($teamId: String, $first: Int) { - channels(userId: "me", teamId: $teamId, first: $first) { - id - stats { - channelId - memberCount - } - } - } - ` - input := graphQLInput{ - OperationName: "channels", - Query: query, - Variables: map[string]any{ - "first": 10, - "teamId": myTeam.Id, - }, - } - - resp, err := th.MakeGraphQLRequest(&input) - require.NoError(t, err) - require.Len(t, resp.Errors, 0) - require.NoError(t, json.Unmarshal(resp.Data, &q)) - require.Len(t, q.Channels, 3) - for _, ch := range q.Channels { - require.Equal(t, ch.ID, ch.Stats.ChannelId) - count, appErr := th.App.GetChannelMemberCount(th.Context, ch.Stats.ChannelId) - require.Nil(t, appErr) - require.Equal(t, float64(count), ch.Stats.MemberCount) - } - }) } func TestGetPrettyDNForUsers(t *testing.T) { diff --git a/api4/schema.graphqls b/api4/schema.graphqls index a976ead790..5af4d4d87c 100644 --- a/api4/schema.graphqls +++ b/api4/schema.graphqls @@ -62,7 +62,6 @@ type Channel { totalMsgCount: Float! totalMsgCountRoot: Float! lastRootPostAt: Float! - stats: ChannelStats extraUpdateAt: Float! props: StringInterface! policyId: String @@ -220,10 +219,3 @@ type Session { props: StringMap! local: Boolean! } - -type ChannelStats { - channelId: String! - memberCount: Float! - guestCount: Float! - pinnedPostCount: Float! -} From 21bf5b5dd5914b585dc545da1d5c09e7cab1599c Mon Sep 17 00:00:00 2001 From: Agniva De Sarker Date: Sat, 15 Oct 2022 01:41:45 +0530 Subject: [PATCH 33/63] MM-47465: Skip flaky TestDeleteChannel (#21392) https://mattermost.atlassian.net/browse/MM-47465 ```release-note NONE ``` --- api4/channel_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/api4/channel_test.go b/api4/channel_test.go index 16e432bf0d..1e1ea4b2e1 100644 --- a/api4/channel_test.go +++ b/api4/channel_test.go @@ -1779,6 +1779,7 @@ func TestSearchGroupChannels(t *testing.T) { } func TestDeleteChannel(t *testing.T) { + t.Skip("MM-47465") th := Setup(t).InitBasic() defer th.TearDown() c := th.Client From 9a857b47b43e73db4e44d574fe81f3c35eea1d95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Boros?= Date: Sat, 15 Oct 2022 06:32:14 +0200 Subject: [PATCH 34/63] feat: nested config value setting using env vars (#21049) Automatic Merge --- config/environment.go | 6 +++ config/environment_test.go | 76 +++++++++++++++++++++++++++++++++++++- 2 files changed, 81 insertions(+), 1 deletion(-) diff --git a/config/environment.go b/config/environment.go index 12e6727484..4ded8d7399 100644 --- a/config/environment.go +++ b/config/environment.go @@ -4,6 +4,7 @@ package config import ( + "encoding/json" "os" "reflect" "strconv" @@ -73,6 +74,11 @@ func applyEnvKey(key, value string, rValueSubject reflect.Value) { } case reflect.SliceOf(reflect.TypeOf("")).Kind(): rFieldValue.Set(reflect.ValueOf(strings.Split(value, " "))) + case reflect.Map: + target := reflect.New(rFieldValue.Type()).Interface() + if err := json.Unmarshal([]byte(value), target); err == nil { + rFieldValue.Set(reflect.ValueOf(target).Elem()) + } } } diff --git a/config/environment_test.go b/config/environment_test.go index fb4b3261ed..e86bcc36e9 100644 --- a/config/environment_test.go +++ b/config/environment_test.go @@ -31,15 +31,54 @@ func TestRemoveEnvOverrides(t *testing.T) { expectedConfig *model.Config }{ { - name: "basic override", + name: "config override", inputConfig: modifiedDefault(func(in *model.Config) { *in.ServiceSettings.TLSMinVer = "1.4" + in.PluginSettings.PluginStates = map[string]*model.PluginState{ + "plugin1": { + Enable: false, + }, + } + in.PluginSettings.Plugins = map[string]map[string]interface{}{ + "com.mattermost.plugin-1": { + "key1": "value1", + }, + "com_mattermost_plugin-2": { + "key2": "value2", + }, + } }), env: map[string]string{ "MM_SERVICESETTINGS_TLSMINVER": "1.5", + "MM_PLUGINSETTINGS_PLUGINSTATES": `{ + "plugin1": { + "Enable": true + } + }`, + "MM_PLUGINSETTINGS_PLUGINS": `{ + "com.mattermost.plugin-1": { + "key1": "other-value" + }, + "com_mattermost_plugin-2": { + "key2": "other-value" + } + }`, }, expectedConfig: modifiedDefault(func(in *model.Config) { *in.ServiceSettings.TLSMinVer = "1.5" + in.PluginSettings.PluginStates = map[string]*model.PluginState{ + "plugin1": { + Enable: true, + }, + } + in.PluginSettings.Plugins = map[string]map[string]interface{}{ + "com.mattermost.plugin-1": { + "key1": "other-value", + }, + "com_mattermost_plugin-2": { + "key2": "other-value", + }, + } }), }, { @@ -102,6 +141,41 @@ func TestRemoveEnvOverrides(t *testing.T) { in.SqlSettings.DataSourceReplicas = []string{"otherthing", "alsothis"} }), }, + { + name: "complex env settings", + inputConfig: modifiedDefault(func(in *model.Config) { + }), + env: map[string]string{ + "MM_PLUGINSETTINGS_PLUGINSTATES": `{ + "com.mattermost.plugin-1": { + "enable": true + } + }`, + "MM_PLUGINSETTINGS_PLUGINS": `{ + "com.mattermost.plugin-1": { + "key": { + "key": "(?PKEY)-(?P\\d{1,6})(?P[,;]*)", + "value": "[$key-$id](https://example.com/?$project-$id)$comma" + } + } + }`, + }, + expectedConfig: modifiedDefault(func(in *model.Config) { + in.PluginSettings.PluginStates = map[string]*model.PluginState{ + "com.mattermost.plugin-1": { + Enable: true, + }, + } + in.PluginSettings.Plugins = map[string]map[string]interface{}{ + "com.mattermost.plugin-1": { + "key": map[string]interface{}{ + "key": "(?PKEY)-(?P\\d{1,6})(?P[,;]*)", + "value": "[$key-$id](https://example.com/?$project-$id)$comma", + }, + }, + } + }), + }, { name: "bad env", inputConfig: modifiedDefault(func(in *model.Config) { From 9e72ca18672419367d7ebf4a9b7f1772cdaf24f5 Mon Sep 17 00:00:00 2001 From: Rohitesh Gupta <1429138+srkgupta@users.noreply.github.com> Date: Tue, 18 Oct 2022 12:44:19 +0530 Subject: [PATCH 35/63] Updated CodeQL to use latest version (#21436) * Update CodeQL to latest version --- .github/codeql/codeql-config.yml | 14 ++++++++++++++ .github/workflows/codeql-analysis.yml | 21 ++++++++++++++------- 2 files changed, 28 insertions(+), 7 deletions(-) create mode 100644 .github/codeql/codeql-config.yml diff --git a/.github/codeql/codeql-config.yml b/.github/codeql/codeql-config.yml new file mode 100644 index 0000000000..3c24100d19 --- /dev/null +++ b/.github/codeql/codeql-config.yml @@ -0,0 +1,14 @@ +name: "CodeQL config" + +query-filters: + - exclude: + problem.severity: + - warning + - recommendation + - exclude: + id: go/log-injection + +paths-ignore: + - templates + - tests + - 'api4/*_local.go' diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 20c839211d..791a45ee95 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -6,7 +6,7 @@ on: branches: [ master ] schedule: - cron: '30 5,17 * * *' - + permissions: contents: read @@ -20,17 +20,24 @@ jobs: strategy: fail-fast: false matrix: - language: [ 'go', 'javascript' ] + language: [ 'go' ] steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@v2 with: languages: ${{ matrix.language }} - debug: true + debug: false + config-file: ./.github/codeql/codeql-config.yml + + # Autobuild attempts to build any compiled languages - name: Autobuild - uses: github/codeql-action/autobuild@v1 + uses: github/codeql-action/autobuild@v2 + + # Perform Analysis - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@v2 From 6aec81eb6e3d8788a328695afe35b1464985f695 Mon Sep 17 00:00:00 2001 From: Ibrahim Serdar Acikgoz Date: Tue, 18 Oct 2022 13:30:22 +0300 Subject: [PATCH 36/63] add nil check for license manager (#21364) --- api4/license.go | 8 +++++++- api4/license_test.go | 34 +++++++++++++++++++++++++++++++--- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/api4/license.go b/api4/license.go index e98f859910..4a56ca74ed 100644 --- a/api4/license.go +++ b/api4/license.go @@ -106,7 +106,13 @@ func addLicense(c *Context, w http.ResponseWriter, r *http.Request) { // skip the restrictions if license is a sanctioned trial if !license.IsSanctionedTrial() && license.IsTrialLicense() { - canStartTrialLicense, err := c.App.Srv().Platform().LicenseManager().CanStartTrial() + lm := c.App.Srv().Platform().LicenseManager() + if lm == nil { + c.Err = model.NewAppError("addLicense", "api.license.upgrade_needed.app_error", nil, "", http.StatusInternalServerError) + return + } + + canStartTrialLicense, err := lm.CanStartTrial() if err != nil { c.Err = model.NewAppError("addLicense", "api.license.add_license.open.app_error", nil, "", http.StatusInternalServerError) return diff --git a/api4/license_test.go b/api4/license_test.go index 05acd6378c..dbf0e62bfc 100644 --- a/api4/license_test.go +++ b/api4/license_test.go @@ -91,9 +91,6 @@ func TestUploadLicenseFile(t *testing.T) { mockLicenseValidator := mocks2.LicenseValidatorIface{} defer testutils.ResetLicenseValidator() - //startTimestamp, err := time.Parse("2 Jan 2006 3:04 pm", "1 Jan 2021 12:00 am") - //require.Nil(t, err) - userCount := 100 mills := model.GetMillis() @@ -125,6 +122,37 @@ func TestUploadLicenseFile(t *testing.T) { require.Equal(t, http.StatusBadRequest, resp.StatusCode) }) + t.Run("try to get gone through trial, with TE build", func(t *testing.T) { + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ExperimentalSettings.RestrictSystemAdmin = false }) + th.App.Srv().Platform().SetLicenseManager(nil) + + mockLicenseValidator := mocks2.LicenseValidatorIface{} + defer testutils.ResetLicenseValidator() + + license := model.License{ + Id: model.NewId(), + Features: &model.Features{ + Users: model.NewInt(100), + }, + Customer: &model.Customer{ + Name: "Test", + }, + StartsAt: model.GetMillis() + 100, + ExpiresAt: model.GetMillis() + 100 + (30*(time.Hour*24) + (time.Hour * 8)).Milliseconds(), + } + + mockLicenseValidator.On("LicenseFromBytes", mock.Anything).Return(&license, nil).Once() + licenseBytes, err := json.Marshal(license) + require.NoError(t, err) + + mockLicenseValidator.On("ValidateLicense", mock.Anything).Return(true, string(licenseBytes)) + utils.LicenseValidator = &mockLicenseValidator + + resp, err := th.SystemAdminClient.UploadLicenseFile([]byte("")) + CheckErrorID(t, err, "api.license.upgrade_needed.app_error") + require.Equal(t, http.StatusInternalServerError, resp.StatusCode) + }) + t.Run("allow uploading sanctioned trials even if server already gone through trial", func(t *testing.T) { mockLicenseValidator := mocks2.LicenseValidatorIface{} defer testutils.ResetLicenseValidator() From b24440c2bdd16cd7f63b9116e6bf08e95cbe4064 Mon Sep 17 00:00:00 2001 From: mattermod Date: Tue, 18 Oct 2022 11:32:34 +0000 Subject: [PATCH 37/63] Update latest version to 7.4.0 --- build/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/Dockerfile b/build/Dockerfile index ab99fe0dcd..55183179d0 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -8,7 +8,7 @@ SHELL ["/bin/bash", "-o", "pipefail", "-c"] ENV PATH="/mattermost/bin:${PATH}" ARG PUID=2000 ARG PGID=2000 -ARG MM_PACKAGE="https://releases.mattermost.com/7.3.0/mattermost-7.3.0-linux-amd64.tar.gz?src=docker" +ARG MM_PACKAGE="https://releases.mattermost.com/7.4.0/mattermost-7.4.0-linux-amd64.tar.gz?src=docker" # # Install needed packages and indirect dependencies RUN apt-get update \ From 3946c6397072ca9e658fa1050514e2e2d9f5fa28 Mon Sep 17 00:00:00 2001 From: Agniva De Sarker Date: Tue, 18 Oct 2022 19:37:40 +0530 Subject: [PATCH 38/63] MM-47612: Skip flaky test TestPluginAPIDeleteUserPreferences (#21425) https://mattermost.atlassian.net/browse/MM-47612 ```release-note NONE ``` --- app/plugin_api_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/app/plugin_api_test.go b/app/plugin_api_test.go index 1068adaa64..d36c4fadc5 100644 --- a/app/plugin_api_test.go +++ b/app/plugin_api_test.go @@ -196,6 +196,7 @@ func TestPluginAPIGetUserPreferences(t *testing.T) { } func TestPluginAPIDeleteUserPreferences(t *testing.T) { + t.Skip("MM-47612") th := Setup(t) defer th.TearDown() api := th.SetupPluginAPI() From 28698abb9f54f0cb173a8226fd378863a5b3ba44 Mon Sep 17 00:00:00 2001 From: Shivashis Padhi Date: Wed, 19 Oct 2022 09:50:41 +0530 Subject: [PATCH 39/63] Apply role filters for /get/users in channel (#21251) --- store/sqlstore/user_store.go | 2 ++ store/storetest/user_store.go | 30 ++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/store/sqlstore/user_store.go b/store/sqlstore/user_store.go index bd89779355..e41d047157 100644 --- a/store/sqlstore/user_store.go +++ b/store/sqlstore/user_store.go @@ -742,6 +742,8 @@ func (us SqlUserStore) GetProfilesInChannel(options *model.UserGetOptions) ([]*m query = query.Where("u.DeleteAt = 0") } + query = applyMultiRoleFilters(query, options.Roles, options.TeamRoles, options.ChannelRoles, us.DriverName() == model.DatabaseDriverPostgres) + queryString, args, err := query.ToSql() if err != nil { return nil, errors.Wrap(err, "get_profiles_in_channel_tosql") diff --git a/store/storetest/user_store.go b/store/storetest/user_store.go index 6d704854b4..3dd02d495a 100644 --- a/store/storetest/user_store.go +++ b/store/storetest/user_store.go @@ -1003,6 +1003,36 @@ func testUserStoreGetProfilesInChannel(t *testing.T, ss store.Store) { require.NoError(t, err) assert.Equal(t, []*model.User{sanitized(u1)}, users) }) + + t.Run("Filter by channel members and channel admins", func(t *testing.T) { + // save admin for c1 + user2Admin, err := ss.User().Save(&model.User{ + Email: MakeEmail(), + Username: "bbb" + model.NewId(), + }) + require.NoError(t, err) + defer func() { require.NoError(t, ss.User().PermanentDelete(user2Admin.Id)) }() + _, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: user2Admin.Id}, -1) + require.NoError(t, nErr) + + _, nErr = ss.Channel().SaveMember(&model.ChannelMember{ + ChannelId: c1.Id, + UserId: user2Admin.Id, + NotifyProps: model.GetDefaultChannelNotifyProps(), + ExplicitRoles: "channel_admin", + }) + require.NoError(t, nErr) + ss.Channel().UpdateMembersRole(c1.Id, []string{user2Admin.Id}) + + users, err := ss.User().GetProfilesInChannel(&model.UserGetOptions{ + InChannelId: c1.Id, + ChannelRoles: []string{model.ChannelAdminRoleId}, + Page: 0, + PerPage: 5, + }) + require.NoError(t, err) + assert.Equal(t, user2Admin.Id, users[0].Id) + }) } func testUserStoreGetProfilesInChannelByAdmin(t *testing.T, ss store.Store, s SqlStore) { From 10a627fac1938d3d91d510082e750b47ccb3b016 Mon Sep 17 00:00:00 2001 From: Agniva De Sarker Date: Wed, 19 Oct 2022 20:51:00 +0530 Subject: [PATCH 40/63] MM-45723: Include DM/GM channels for user indexing (#21391) There were 2 separate issues here: 1. We were excluding DM/GM channels for user indexing. Not sure why. This prevented users to be shown as channel members in autocomplete results in both Bleve and Elasticsearch. 2. We were incorrectly deciding to search across the team based on if the current channel is in the list of allowed channels or not. But we were incorrectly wiping the list of allowed channels even if a channelid was passed. This makes it work same as a DB search. One catch is that for the correct results to appear, a re-indexing is required. ```release-note Autocompletion results using Elasticsearch or Bleve will correctly show a user as a channel member in DM/GM channels. To force this change to appear, a re-indexing will be necessary. ``` --- store/searchlayer/user_layer.go | 23 +++++++++++++++++++---- store/sqlstore/user_store.go | 11 ++++++++++- store/storetest/user_store.go | 25 +++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 5 deletions(-) diff --git a/store/searchlayer/user_layer.go b/store/searchlayer/user_layer.go index 2c63dabd3a..268b18cacc 100644 --- a/store/searchlayer/user_layer.go +++ b/store/searchlayer/user_layer.go @@ -37,7 +37,7 @@ func (s *SearchUserStore) deleteUserIndex(user *model.User) { func (s *SearchUserStore) Search(teamId, term string, options *model.UserSearchOptions) ([]*model.User, error) { for _, engine := range s.rootStore.searchEngine.GetActiveEngines() { if engine.IsSearchEnabled() { - listOfAllowedChannels, nErr := s.getListOfAllowedChannelsForTeam(teamId, options.ViewRestrictions) + listOfAllowedChannels, nErr := s.getListOfAllowedChannels(teamId, "", options.ViewRestrictions) if nErr != nil { mlog.Warn("Encountered error on Search.", mlog.String("search_engine", engine.GetName()), mlog.Err(nErr)) continue @@ -148,7 +148,7 @@ func (s *SearchUserStore) autocompleteUsersInChannelByEngine(engine searchengine return autocomplete, nil } -// getListOfAllowedChannelsForTeam return the list of allowed channels to search user based on the +// getListOfAllowedChannels return the list of allowed channels to search user based on the // next scenarios: // - If there isn't view restrictions (team or channel) and no team id to filter them, then all // channels are allowed (nil return) @@ -159,7 +159,7 @@ func (s *SearchUserStore) autocompleteUsersInChannelByEngine(engine searchengine // - If we receive channels restrictions we get: // - If we don't have team id, we get those restricted channels (guest accounts and quick search) // - If we have a team id then we only return those restricted channels that belongs to that team -func (s *SearchUserStore) getListOfAllowedChannelsForTeam(teamId string, viewRestrictions *model.ViewUsersRestrictions) ([]string, error) { +func (s *SearchUserStore) getListOfAllowedChannels(teamId, channelId string, viewRestrictions *model.ViewUsersRestrictions) ([]string, error) { var listOfAllowedChannels []string if viewRestrictions == nil && teamId == "" { // nil return without error means all channels are allowed @@ -174,6 +174,20 @@ func (s *SearchUserStore) getListOfAllowedChannelsForTeam(teamId string, viewRes for _, channel := range channels { listOfAllowedChannels = append(listOfAllowedChannels, channel.Id) } + + if channelId != "" { + ch, err := s.rootStore.Channel().Get(channelId, true) + if err != nil { + return nil, errors.Wrapf(err, "failed to get channel with id: %s", channelId) + } + // Check if DM/GM channel, and add to the list. + // This is because GetTeamChannels does not return DM/GM channels. + // And since the channelId is passed from the API layer, it is already + // auth checked to confirm that the user has permission. + if ch.IsGroupOrDirect() { + listOfAllowedChannels = append(listOfAllowedChannels, channelId) + } + } return listOfAllowedChannels, nil } @@ -196,7 +210,7 @@ func (s *SearchUserStore) getListOfAllowedChannelsForTeam(teamId string, viewRes func (s *SearchUserStore) AutocompleteUsersInChannel(teamId, channelId, term string, options *model.UserSearchOptions) (*model.UserAutocompleteInChannel, error) { for _, engine := range s.rootStore.searchEngine.GetActiveEngines() { if engine.IsAutocompletionEnabled() { - listOfAllowedChannels, nErr := s.getListOfAllowedChannelsForTeam(teamId, options.ViewRestrictions) + listOfAllowedChannels, nErr := s.getListOfAllowedChannels(teamId, channelId, options.ViewRestrictions) if nErr != nil { mlog.Warn("Encountered error on AutocompleteUsersInChannel.", mlog.String("search_engine", engine.GetName()), mlog.Err(nErr)) continue @@ -205,6 +219,7 @@ func (s *SearchUserStore) AutocompleteUsersInChannel(teamId, channelId, term str return &model.UserAutocompleteInChannel{}, nil } options.ListOfAllowedChannels = listOfAllowedChannels + autocomplete, nErr := s.autocompleteUsersInChannelByEngine(engine, teamId, channelId, term, options) if nErr != nil { mlog.Warn("Encountered error on AutocompleteUsersInChannel.", mlog.String("search_engine", engine.GetName()), mlog.Err(nErr)) diff --git a/store/sqlstore/user_store.go b/store/sqlstore/user_store.go index e41d047157..9eb8535d90 100644 --- a/store/sqlstore/user_store.go +++ b/store/sqlstore/user_store.go @@ -1805,7 +1805,16 @@ func (us SqlUserStore) GetUsersBatchForIndexing(startTime int64, startFileID str `). From("ChannelMembers cm"). Join("Channels c ON cm.ChannelId = c.Id"). - Where(sq.Eq{"c.Type": model.ChannelTypeOpen, "cm.UserId": userIds}). + Where(sq.And{ + sq.Eq{ + "cm.UserId": userIds, + }, + sq.Or{ + sq.Eq{"c.Type": model.ChannelTypeOpen}, + sq.Eq{"c.Type": model.ChannelTypeDirect}, + sq.Eq{"c.Type": model.ChannelTypeGroup}, + }, + }). ToSql() if err != nil { return nil, errors.Wrap(err, "GetUsersBatchForIndexing_ToSql2") diff --git a/store/storetest/user_store.go b/store/storetest/user_store.go index 3dd02d495a..c938056249 100644 --- a/store/storetest/user_store.go +++ b/store/storetest/user_store.go @@ -4912,10 +4912,35 @@ func testUserStoreGetUsersBatchForIndexing(t *testing.T, ss store.Store) { }) require.NoError(t, err) + cDM := &model.Channel{ + Name: model.NewId() + "__" + model.NewId(), + Type: model.ChannelTypeDirect, + } + cm1 := &model.ChannelMember{ + UserId: u3.Id, + ChannelId: cDM.Id, + NotifyProps: model.GetDefaultChannelNotifyProps(), + } + cm2 := &model.ChannelMember{ + UserId: u2.Id, + ChannelId: cDM.Id, + NotifyProps: model.GetDefaultChannelNotifyProps(), + } + cDM, nErr = ss.Channel().SaveDirectChannel(cDM, cm1, cm2) + require.NoError(t, nErr) + // Getting all users res1List, err := ss.User().GetUsersBatchForIndexing(u1.CreateAt-1, "", 100) require.NoError(t, err) assert.Len(t, res1List, 3) + for _, user := range res1List { + switch user.Id { + case u2.Id: + assert.ElementsMatch(t, user.ChannelsIds, []string{cPub1.Id, cPub2.Id, cDM.Id}) + case u3.Id: + assert.ElementsMatch(t, user.ChannelsIds, []string{cPub2.Id, cDM.Id}) + } + } // Testing pagination res2List, err := ss.User().GetUsersBatchForIndexing(u1.CreateAt-1, "", 1) From 5730d3da6e971934e26d898df12da885e3986c80 Mon Sep 17 00:00:00 2001 From: Agniva De Sarker Date: Thu, 20 Oct 2022 00:53:20 +0530 Subject: [PATCH 41/63] MM-47250: Add new system console section (#21333) * MM-47250: Add new system console section We add a new section in system console for products. ```release-note Added a new section in system console for products. For now, it only contains boards specific settings. ``` * fix unit tests ```release-note NONE ``` * change comment ```release-note NONE ``` --- app/permissions_migrations.go | 22 ++++++++++++++++++++++ model/config.go | 12 ++++++++++++ model/migration.go | 1 + model/permission.go | 18 ++++++++++++++++++ model/role.go | 3 +++ services/telemetry/telemetry.go | 5 +++++ testlib/store.go | 1 + 7 files changed, 62 insertions(+) diff --git a/app/permissions_migrations.go b/app/permissions_migrations.go index 613adc927f..2b638be162 100644 --- a/app/permissions_migrations.go +++ b/app/permissions_migrations.go @@ -991,6 +991,27 @@ func (a *App) getPlaybooksPermissionsAddManageRoles() (permissionsMap, error) { return transformations, nil } +func (a *App) getProductsBoardsPermissions() (permissionsMap, error) { + transformations := []permissionTransformation{} + + permissionsProductsRead := []string{model.PermissionSysconsoleReadProductsBoards.Id} + permissionsProductsWrite := []string{model.PermissionSysconsoleWriteProductsBoards.Id} + + // Give the new subsection READ permissions to any user with SYSTEM_MANAGER + transformations = append(transformations, permissionTransformation{ + On: permissionOr(isRole(model.SystemManagerRoleId)), + Add: permissionsProductsRead, + }) + + // Give the new subsection WRITE permissions to any user with SYSTEM_ADMIN + transformations = append(transformations, permissionTransformation{ + On: permissionOr(isRole(model.SystemAdminRoleId)), + Add: permissionsProductsWrite, + }) + + return transformations, nil +} + // DoPermissionsMigrations execute all the permissions migrations need by the current version. func (a *App) DoPermissionsMigrations() error { return a.Srv().doPermissionsMigrations() @@ -1032,6 +1053,7 @@ func (s *Server) doPermissionsMigrations() error { {Key: model.MigrationKeyAddPlaybooksPermissions, Migration: a.getAddPlaybooksPermissions}, {Key: model.MigrationKeyAddCustomUserGroupsPermissions, Migration: a.getAddCustomUserGroupsPermissions}, {Key: model.MigrationKeyAddPlayboosksManageRolesPermissions, Migration: a.getPlaybooksPermissionsAddManageRoles}, + {Key: model.MigrationKeyAddProductsBoardsPermissions, Migration: a.getProductsBoardsPermissions}, } roles, err := s.Store().Role().GetAll() diff --git a/model/config.go b/model/config.go index 886b7e5820..bccc3904a1 100644 --- a/model/config.go +++ b/model/config.go @@ -2747,6 +2747,16 @@ func (s *CloudSettings) SetDefaults() { } } +type ProductSettings struct { + EnablePublicSharedBoards *bool +} + +func (s *ProductSettings) SetDefaults() { + if s.EnablePublicSharedBoards == nil { + s.EnablePublicSharedBoards = NewBool(false) + } +} + type PluginState struct { Enable bool } @@ -3136,6 +3146,7 @@ type Config struct { DataRetentionSettings DataRetentionSettings MessageExportSettings MessageExportSettings JobSettings JobSettings + ProductSettings ProductSettings PluginSettings PluginSettings DisplaySettings DisplaySettings GuestAccountsSettings GuestAccountsSettings @@ -3235,6 +3246,7 @@ func (o *Config) SetDefaults() { o.ThemeSettings.SetDefaults() o.ClusterSettings.SetDefaults() o.PluginSettings.SetDefaults(o.LogSettings) + o.ProductSettings.SetDefaults() o.AnalyticsSettings.SetDefaults() o.ComplianceSettings.SetDefaults() o.LocalizationSettings.SetDefaults() diff --git a/model/migration.go b/model/migration.go index 4958f80e85..e0e9ae2267 100644 --- a/model/migration.go +++ b/model/migration.go @@ -38,4 +38,5 @@ const ( MigrationKeyAddPlaybooksPermissions = "playbooks_permissions" MigrationKeyAddCustomUserGroupsPermissions = "custom_groups_permissions" MigrationKeyAddPlayboosksManageRolesPermissions = "playbooks_manage_roles" + MigrationKeyAddProductsBoardsPermissions = "products_boards" ) diff --git a/model/permission.go b/model/permission.go index 65f8edaca8..76cf07c872 100644 --- a/model/permission.go +++ b/model/permission.go @@ -354,6 +354,9 @@ var PermissionRunManageProperties *Permission var PermissionRunManageMembers *Permission var PermissionRunView *Permission +var PermissionSysconsoleReadProductsBoards *Permission +var PermissionSysconsoleWriteProductsBoards *Permission + // General permission that encompasses all system admin functions // in the future this could be broken up to allow access to some // admin functions but not others @@ -2070,6 +2073,19 @@ func initializePermissions() { PermissionScopeRun, } + PermissionSysconsoleReadProductsBoards = &Permission{ + "sysconsole_read_products_boards", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleWriteProductsBoards = &Permission{ + "sysconsole_write_products_boards", + "", + "", + PermissionScopeSystem, + } + SysconsoleReadPermissions = []*Permission{ PermissionSysconsoleReadAboutEditionAndLicense, PermissionSysconsoleReadBilling, @@ -2125,6 +2141,7 @@ func initializePermissions() { PermissionSysconsoleReadExperimentalFeatures, PermissionSysconsoleReadExperimentalFeatureFlags, PermissionSysconsoleReadExperimentalBleve, + PermissionSysconsoleReadProductsBoards, } SysconsoleWritePermissions = []*Permission{ @@ -2182,6 +2199,7 @@ func initializePermissions() { PermissionSysconsoleWriteExperimentalFeatures, PermissionSysconsoleWriteExperimentalFeatureFlags, PermissionSysconsoleWriteExperimentalBleve, + PermissionSysconsoleWriteProductsBoards, } SystemScopedPermissionsMinusSysconsole := []*Permission{ diff --git a/model/role.go b/model/role.go index 7823f133b4..ac3fa3204e 100644 --- a/model/role.go +++ b/model/role.go @@ -261,6 +261,7 @@ func init() { PermissionSysconsoleReadExperimentalFeatures.Id, PermissionSysconsoleReadExperimentalFeatureFlags.Id, PermissionSysconsoleReadExperimentalBleve.Id, + PermissionSysconsoleReadProductsBoards.Id, } SystemManagerDefaultPermissions = []string{ @@ -339,6 +340,8 @@ func init() { PermissionSysconsoleWriteIntegrationsBotAccounts.Id, PermissionSysconsoleWriteIntegrationsGif.Id, PermissionSysconsoleWriteIntegrationsCors.Id, + PermissionSysconsoleReadProductsBoards.Id, + PermissionSysconsoleWriteProductsBoards.Id, } SystemCustomGroupAdminDefaultPermissions = []string{ diff --git a/services/telemetry/telemetry.go b/services/telemetry/telemetry.go index f19e5a4188..821b4c8a91 100644 --- a/services/telemetry/telemetry.go +++ b/services/telemetry/telemetry.go @@ -67,6 +67,7 @@ const ( TrackConfigBleve = "config_bleve" TrackConfigExport = "config_export" TrackFeatureFlags = "config_feature_flags" + TrackConfigProducts = "products" TrackPermissionsGeneral = "permissions_general" TrackPermissionsSystemScheme = "permissions_system_scheme" TrackPermissionsTeamSchemes = "permissions_team_schemes" @@ -827,6 +828,10 @@ func (ts *TelemetryService) trackConfig() { "retention_days": *cfg.ExportSettings.RetentionDays, }) + ts.SendTelemetry(TrackConfigProducts, map[string]any{ + "enable_public_shared_boards": *cfg.ProductSettings.EnablePublicSharedBoards, + }) + // Convert feature flags to map[string]any for sending flags := cfg.FeatureFlags.ToMap() interfaceFlags := make(map[string]any) diff --git a/testlib/store.go b/testlib/store.go index 5566989362..800764da07 100644 --- a/testlib/store.go +++ b/testlib/store.go @@ -69,6 +69,7 @@ func GetMockStoreForSetupFunctions() *mocks.Store { systemStore.On("GetByName", model.MigrationKeyAddCustomUserGroupsPermissions).Return(&model.System{Name: model.MigrationKeyAddCustomUserGroupsPermissions, Value: "true"}, nil) systemStore.On("GetByName", model.MigrationKeyAddPlayboosksManageRolesPermissions).Return(&model.System{Name: model.MigrationKeyAddPlayboosksManageRolesPermissions, Value: "true"}, nil) systemStore.On("GetByName", "CustomGroupAdminRoleCreationMigrationComplete").Return(&model.System{Name: model.MigrationKeyAddPlayboosksManageRolesPermissions, Value: "true"}, nil) + systemStore.On("GetByName", "products_boards").Return(&model.System{Name: "products_boards", Value: "true"}, nil) systemStore.On("InsertIfExists", mock.AnythingOfType("*model.System")).Return(&model.System{}, nil).Once() systemStore.On("Save", mock.AnythingOfType("*model.System")).Return(nil) From 5b2b2be753a7544c08b600cbc31af34a3ef38f90 Mon Sep 17 00:00:00 2001 From: jprusch Date: Wed, 19 Oct 2022 06:20:55 +0200 Subject: [PATCH 42/63] Translated using Weblate (German) Currently translated at 100.0% (2386 of 2386 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/de/ Translated using Weblate (German) Currently translated at 100.0% (2386 of 2386 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/de/ Translated using Weblate (German) Currently translated at 100.0% (2385 of 2385 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/de/ Translated using Weblate (German) Currently translated at 100.0% (2385 of 2385 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/de/ Translated using Weblate (German) Currently translated at 100.0% (2384 of 2384 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/de/ Translated using Weblate (German) Currently translated at 100.0% (2382 of 2382 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/de/ --- i18n/de.json | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/i18n/de.json b/i18n/de.json index ae6d93035c..923ae5eedd 100644 --- a/i18n/de.json +++ b/i18n/de.json @@ -1777,7 +1777,7 @@ }, { "id": "api.templates.email_change_verify_body.title", - "translation": "Du hast deine E-Mail-Adresse geändert" + "translation": "Du hast deine E-Mail-Adresse erfolgreich geändert" }, { "id": "api.templates.email_change_verify_subject", @@ -4801,11 +4801,11 @@ }, { "id": "web.error.unsupported_browser.min_browser_version.edge", - "translation": "Version 44+" + "translation": "Version 95+" }, { "id": "web.error.unsupported_browser.min_browser_version.chrome", - "translation": "Version 100+" + "translation": "Version 102+" }, { "id": "web.error.unsupported_browser.learn_more", @@ -8380,7 +8380,7 @@ }, { "id": "app.notification.body.dm.subTitle", - "translation": "Währen du weg warst, hat dir {{.SenderName}} eine neue Direktnachricht gesendet." + "translation": "Während du weg warst, hat dir {{.SenderName}} eine neue Direktnachricht gesendet." }, { "id": "app.license.generate_renewal_token.no_license", @@ -9537,5 +9537,21 @@ { "id": "app.user.get_badge_count.app_error", "translation": "Wir konnten den Nachrichtenzähler für den Benutzer nicht abfragen." + }, + { + "id": "app.job.error", + "translation": "Fehler bei der Auftragsausführung." + }, + { + "id": "app.last_accessible_file.app_error", + "translation": "Fehler beim Abrufen der letzten zugänglichen Datei" + }, + { + "id": "app.file.cloud.get.app_error", + "translation": "Die Datei kann nicht abgerufen werden, da sie das Limit des Cloud-Plans überschritten hat." + }, + { + "id": "model.group.name.reserved_name.app_error", + "translation": "Gruppenname existiert bereits als reservierter Name" } ] From 46dde366d913d0ee0e10d7fd95c330311b487007 Mon Sep 17 00:00:00 2001 From: Tom De Moor Date: Wed, 19 Oct 2022 06:20:55 +0200 Subject: [PATCH 43/63] Translated using Weblate (Dutch) Currently translated at 99.9% (2384 of 2386 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/nl/ Translated using Weblate (Dutch) Currently translated at 100.0% (2386 of 2386 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/nl/ Translated using Weblate (Dutch) Currently translated at 100.0% (2382 of 2382 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/nl/ --- i18n/nl.json | 36 ++++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/i18n/nl.json b/i18n/nl.json index 6f883eaa1a..2695bddadb 100644 --- a/i18n/nl.json +++ b/i18n/nl.json @@ -1777,7 +1777,7 @@ }, { "id": "api.templates.email_change_verify_body.title", - "translation": "Je hebt je e-mailadres bijgewerkt" + "translation": "Je hebt je e-mailadres succesvol bijgewerkt" }, { "id": "api.templates.email_change_verify_subject", @@ -4325,7 +4325,7 @@ }, { "id": "model.post.is_valid.create_at.app_error", - "translation": "Aangemaakt op moet een geldige tijd bevatten" + "translation": "Aangemaakt op moet een geldige tijd bevatten." }, { "id": "model.post.is_valid.file_ids.app_error", @@ -4781,11 +4781,11 @@ }, { "id": "web.error.unsupported_browser.min_browser_version.edge", - "translation": "Versie 44+" + "translation": "Versie 95+" }, { "id": "web.error.unsupported_browser.min_browser_version.chrome", - "translation": "Versie 100+" + "translation": "Versie 106+" }, { "id": "web.error.unsupported_browser.learn_more", @@ -9537,5 +9537,33 @@ { "id": "api.templates.delinquency_90.title", "translation": "Jouw Mattermost-werkruimte werd gedowngraded" + }, + { + "id": "app.job.error", + "translation": "Fout tijdens het uitvoeren van de opdracht." + }, + { + "id": "model.group.name.reserved_name.app_error", + "translation": "groepsnaam bestaat al als een gereserveerde naam" + }, + { + "id": "app.plugin.product_mode.app_error", + "translation": "Plugin {{.Name}} kan niet worden ingeschakeld in de productmodus." + }, + { + "id": "app.last_accessible_file.app_error", + "translation": "Fout bij het ophalen van het laatst toegankelijke bestand" + }, + { + "id": "app.file.cloud.get.app_error", + "translation": "Kan het bestand niet ophalen omdat dit bestand over de limiet van het cloud plan is." + }, + { + "id": "api.team.invite_guests_to_channels.license.error", + "translation": "Jouw licentie ondersteunt geen gastaccounts" + }, + { + "id": "api.team.invite_guests_to_channels.disabled.error", + "translation": "Gastaccounts zijn uitgeschakeld" } ] From 1ee8f17f8e7058f35ec907263c913b1d0cc1215e Mon Sep 17 00:00:00 2001 From: Kaya Zeren Date: Wed, 19 Oct 2022 06:20:55 +0200 Subject: [PATCH 44/63] Translated using Weblate (Turkish) Currently translated at 100.0% (2386 of 2386 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/tr/ Translated using Weblate (Turkish) Currently translated at 100.0% (2385 of 2385 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/tr/ Translated using Weblate (Turkish) Currently translated at 100.0% (2384 of 2384 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/tr/ Translated using Weblate (Turkish) Currently translated at 100.0% (2382 of 2382 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/tr/ --- i18n/tr.json | 36 ++++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/i18n/tr.json b/i18n/tr.json index 563015338b..bdb2ca0902 100644 --- a/i18n/tr.json +++ b/i18n/tr.json @@ -2725,7 +2725,7 @@ }, { "id": "app.import.validate_team_import_data.name_reserved.error", - "translation": "Takım adında ayrılmış bir sözcük var." + "translation": "Takım adında sistem kullanımına ayrılmış bir sözcük var." }, { "id": "app.import.validate_team_import_data.scheme_invalid.error", @@ -5577,7 +5577,7 @@ }, { "id": "api.ldap_groups.existing_reserved_name_error", - "translation": "aynı adlı bir grup zaten ayrılmış bir ad olarak var" + "translation": "aynı adlı bir grup zaten sistem kullanımına ayrılmış bir ad olarak var" }, { "id": "api.ldap_groups.existing_user_name_error", @@ -5849,11 +5849,11 @@ }, { "id": "web.error.unsupported_browser.min_browser_version.chrome", - "translation": "Sürüm 100+" + "translation": "Sürüm 106+" }, { "id": "web.error.unsupported_browser.min_browser_version.edge", - "translation": "Sürüm 44+" + "translation": "Sürüm 95+" }, { "id": "web.error.unsupported_browser.min_browser_version.firefox", @@ -9537,5 +9537,33 @@ { "id": "app.user.get_badge_count.app_error", "translation": "Kullanıcının nişan sayısını alamadık." + }, + { + "id": "app.job.error", + "translation": "Görev yürütülürken sorun çıktı." + }, + { + "id": "app.last_accessible_file.app_error", + "translation": "Erişilebilen son dosya alınırken sorun çıktı" + }, + { + "id": "app.file.cloud.get.app_error", + "translation": "Cloud tarifesinin sınırlarını aştığından dosya alınamadı." + }, + { + "id": "model.group.name.reserved_name.app_error", + "translation": "aynı adlı bir grup zaten sistem kullanımına ayrılmış bir ad olarak var" + }, + { + "id": "app.plugin.product_mode.app_error", + "translation": "{{.Name}} uygulama eki ürün kipinde etkinleştirilemez." + }, + { + "id": "api.team.invite_guests_to_channels.license.error", + "translation": "Lisansınız konuk hesaplarının kullanılmasını desteklemiyor" + }, + { + "id": "api.team.invite_guests_to_channels.disabled.error", + "translation": "Konuk hesapları devre dışı bırakılmış" } ] From bab91b41f8f511398c0ed001cc423efc786498b8 Mon Sep 17 00:00:00 2001 From: Dennis Cheng Date: Wed, 19 Oct 2022 06:20:56 +0200 Subject: [PATCH 45/63] Translated using Weblate (Chinese (Traditional)) Currently translated at 75.7% (1805 of 2382 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/zh_Hant/ --- i18n/zh-TW.json | 58 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 55 insertions(+), 3 deletions(-) diff --git a/i18n/zh-TW.json b/i18n/zh-TW.json index a24b40c159..0f3fda947e 100644 --- a/i18n/zh-TW.json +++ b/i18n/zh-TW.json @@ -421,7 +421,7 @@ }, { "id": "api.command_channel_header.update_channel.app_error", - "translation": "更新當前頻道時錯誤。" + "translation": "更新當前頻道名稱時錯誤。" }, { "id": "api.command_channel_purpose.channel.app_error", @@ -453,7 +453,7 @@ }, { "id": "api.command_channel_purpose.update_channel.app_error", - "translation": "更新當前頻道時錯誤。" + "translation": "更新當前頻道用途時錯誤。" }, { "id": "api.command_channel_remove.channel.app_error", @@ -619,7 +619,7 @@ }, { "id": "api.command_help.desc", - "translation": "開啟 Mattermost 說明頁面" + "translation": "顯示 Mattermost 說明頁面" }, { "id": "api.command_help.name", @@ -7302,5 +7302,57 @@ { "id": "api.cloud.teams_limit_reached.create", "translation": "無法建立團隊,已達到團隊數量上限" + }, + { + "id": "api.command_custom_status.success", + "translation": "你的狀態已設為 “{{.EmojiName}} {{.StatusMessage}}”。您可以從頻道側邊欄中的狀態彈出視窗更改您的狀態。" + }, + { + "id": "api.command_custom_status.name", + "translation": "狀態" + }, + { + "id": "api.command_custom_status.hint", + "translation": "[:emoji_name:] [status_message] 或清除" + }, + { + "id": "api.command_custom_status.desc", + "translation": "設定或清除您的狀態" + }, + { + "id": "api.command_custom_status.clear.success", + "translation": "狀態已清除。" + }, + { + "id": "api.command_custom_status.clear.app_error", + "translation": "清除狀態錯誤。" + }, + { + "id": "api.command_custom_status.app_error", + "translation": "設定狀態錯誤。" + }, + { + "id": "api.command_channel_purpose.update_channel.max_length", + "translation": "輸入的文本超出字數限制。頻道用途不得超過 {{.MaxLength}} 個字數。" + }, + { + "id": "api.command_channel_header.update_channel.max_length", + "translation": "輸入的文本超出字數限制。頻道標題不得超過 {{.MaxLength}} 個字數。" + }, + { + "id": "api.cloud.delinquency_email.missing_email_to_trigger", + "translation": "缺少發送拖欠電子郵件的必要欄位。" + }, + { + "id": "api.cloud.cws_webhook_event_missing_error", + "translation": "Webhook 事件未處理。事件可能遺失或無效。" + }, + { + "id": "api.channel.create_channel.direct_channel.team_restricted_error", + "translation": "無法在這些使用者間創建直連頻道因為他們不屬於同一個團隊。" + }, + { + "id": "api.admin.saml.failure_reset_authdata_to_email.app_error", + "translation": "無法寄出重設AuthData欄位失敗的電子郵件。" } ] From c9103687de1b0b17b50fee877bcfb32227cda2e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=B3th=20Csaba=20//=20Online=20ERP=20Hungary=20Kft?= Date: Wed, 19 Oct 2022 06:20:56 +0200 Subject: [PATCH 46/63] Translated using Weblate (Hungarian) Currently translated at 98.3% (2346 of 2385 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/hu/ Translated using Weblate (Hungarian) Currently translated at 98.2% (2343 of 2385 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/hu/ Translated using Weblate (Hungarian) Currently translated at 98.3% (2342 of 2382 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/hu/ --- i18n/hu.json | 48 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 44 insertions(+), 4 deletions(-) diff --git a/i18n/hu.json b/i18n/hu.json index b10693e9f8..2f7e270a62 100644 --- a/i18n/hu.json +++ b/i18n/hu.json @@ -3125,7 +3125,7 @@ }, { "id": "oauth.gitlab.tos.error", - "translation": "A GitLab Általános Szerződési Feltételei frissültek. Kérjük, látogassa meg a gitlab.com webhelyet, hogy elfogadja őket, majd próbáljon meg újra bejelentkezni a Mattermost-ba." + "translation": "A GitLab Általános Szerződési Feltételei frissültek. Kérjük, látogassa meg a {{.URL}} webhelyet, hogy elfogadja őket, majd próbáljon meg újra bejelentkezni a Mattermost-ba." }, { "id": "model.websocket_client.connect_fail.app_error", @@ -5469,15 +5469,15 @@ }, { "id": "api.templates.payment_failed.title", - "translation": "Sikertelen fizetés" + "translation": "A fizetési művelet nem sikerült" }, { "id": "api.templates.payment_failed.subject", - "translation": "Művelet szükséges: Sikertelen fizetés a Mattermost Cloudra" + "translation": "Művelet szükséges: Sikertelen fizetés a Mattermost {{.Plan}} csomagra" }, { "id": "api.templates.payment_failed.info3", - "translation": "A Mattermost Cloud zavartalan előfizetésének biztosítása érdekében kérjük, vegye fel a kapcsolatot a pénzintézetével a probléma megoldása érdekében, vagy frissítse fizetési adatait. A fizetési adatok frissítése után a Mattermost megpróbálja rendezni a fennmaradó egyenleget." + "translation": "A Mattermost {{.Plan}} zavartalan elérésének biztosítása érdekében kérjük, vegye fel a kapcsolatot a pénzintézetével a probléma megoldása érdekében, vagy frissítse fizetési adatait. A fizetési adatok frissítése után a Mattermost megpróbálja rendezni a fennmaradó egyenleget." }, { "id": "api.templates.payment_failed.info2", @@ -9354,5 +9354,45 @@ { "id": "api.templates.delinquency_30.subject", "translation": "Lépjen most, hogy megtarthassa a Mattermost {{.Plan}} funkcióit" + }, + { + "id": "app.notify_admin.send_notification_post.app_error", + "translation": "Nem sikerült elküldeni az értesítési bejegyzést." + }, + { + "id": "app.notify_admin.save.app_error", + "translation": "Nem sikerült elmenteni az értesítési beállításokat." + }, + { + "id": "app.job.error", + "translation": "Hiba a művelet végrehajtása közben." + }, + { + "id": "app.cloud.get_current_plan_name.app_error", + "translation": "Nem sikerült lekérni a jelenlegi előfizetési csomag nevét" + }, + { + "id": "api.templates.delinquency_30.button", + "translation": "Fizetés frissítése" + }, + { + "id": "api.templates.delinquency_30.bullet.plugins", + "translation": "Aktív bővítmények és integrációk" + }, + { + "id": "api.cloud.delinquency_email.missing_email_to_trigger", + "translation": "Hiányzó kötelező mezők a késedelmes e-mail küldéséhez." + }, + { + "id": "app.file.cloud.get.app_error", + "translation": "Nem sikerült lekérni a fájlt, mivel az meghaladta a felhőcsomag határértékét." + }, + { + "id": "app.user.get_badge_count.app_error", + "translation": "Nem sikerült lekérni a felhasználó jelvény számlálóját." + }, + { + "id": "model.group.name.reserved_name.app_error", + "translation": "csoport név létezik mint lefoglalt név" } ] From 62626d31e328703156e1bed8746a5e3be6f24f33 Mon Sep 17 00:00:00 2001 From: Tilto_ Date: Wed, 19 Oct 2022 06:20:56 +0200 Subject: [PATCH 47/63] Translated using Weblate (Korean) Currently translated at 83.3% (1986 of 2384 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/ko/ --- i18n/ko.json | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/i18n/ko.json b/i18n/ko.json index 6efc6c5e61..79e6d396d5 100644 --- a/i18n/ko.json +++ b/i18n/ko.json @@ -619,7 +619,7 @@ }, { "id": "api.command_help.desc", - "translation": "Mattermost 도움말 페이지 열기" + "translation": "Mattermost 도움말 보기" }, { "id": "api.command_help.name", @@ -7934,5 +7934,41 @@ { "id": "api.cloud.teams_limit_reached.create", "translation": "팀 제한에 도달했기 때문에 팀을 만들 수 없습니다" + }, + { + "id": "api.command_marketplace.desc", + "translation": "마켓플레이스 열기" + }, + { + "id": "api.command_help.success", + "translation": "Mattermost는 도구와 팀을 넘나드는 안전한 소통, 협업, 작업 조율을 위한 오픈소스 플랫폼입니다.\nMattermost는 세가지 핵심 도구를 갖고 있습니다:\n\n**채널** - 1:1 및 그룹 채팅을 통해 팀과의 소통을 유지하세요.\n**[플레이북](/playbooks)** - 반복가능한 작업들을 구성하여 구체적이고 예측 가능한 결과를 만드세요.\n**[보드](/boards)** - 팀의 핵심 마일스톤에 도달하기 위해 칸반 보드에서 작업을 만들고 프로젝트를 관리하세요.\n\n[문서 및 가이드 살펴보기]({{.HelpLink}})" + }, + { + "id": "api.cloud.delinquency_email.missing_email_to_trigger", + "translation": "연체 이메일을 보내기 위한 필수 항목이 누락되었습니다." + }, + { + "id": "api.error_set_first_admin_complete_setup", + "translation": "스토어에서 첫 번째 관리자의 설정을 저장하는 동안 오류가 발생했습니다." + }, + { + "id": "api.error_get_first_admin_visit_marketplace_status", + "translation": "스토어에서 첫 번째 관리자의 마켓플레이스 방문 상태를 검색하는 동안 오류가 발생했습니다." + }, + { + "id": "api.error_get_first_admin_complete_setup", + "translation": "스토어에서 첫 번째 관리자 설정을 검색하는 동안 오류가 발생했습니다." + }, + { + "id": "api.command_remote.invite_summary", + "translation": "AES-256으로 암호화된 초대를 비밀번호와 함께 외부 Mattermost 시스템 관리자에게 보냅니다. 초대를 받기 위해 `{{.Command}}` 슬래시 명령어를 사용합니다.\n\n```\n{{.Invitation}}\n```\n\n**보안 연결이 다음을 통해 Mattermost에 액세스할 수 있는지 확인하십시오** {{.SiteURL}}" + }, + { + "id": "api.command_marketplace.unsupported.app_error", + "translation": "마켓플레이스 명령어는 현재 기기에서 지원하지 않습니다." + }, + { + "id": "api.command_marketplace.name", + "translation": "마켓플레이스" } ] From df62328291756d443be588d29b5d2691e809bedf Mon Sep 17 00:00:00 2001 From: master7 Date: Wed, 19 Oct 2022 06:20:57 +0200 Subject: [PATCH 48/63] Translated using Weblate (Polish) Currently translated at 100.0% (2386 of 2386 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/pl/ Translated using Weblate (Polish) Currently translated at 100.0% (2386 of 2386 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/pl/ Translated using Weblate (Polish) Currently translated at 100.0% (2385 of 2385 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/pl/ Translated using Weblate (Polish) Currently translated at 100.0% (2384 of 2384 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/pl/ --- i18n/pl.json | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/i18n/pl.json b/i18n/pl.json index de2e7d8a68..48fe1d9b82 100644 --- a/i18n/pl.json +++ b/i18n/pl.json @@ -4761,11 +4761,11 @@ }, { "id": "web.error.unsupported_browser.min_browser_version.edge", - "translation": "Wersja 44+" + "translation": "Wersja 95+" }, { "id": "web.error.unsupported_browser.min_browser_version.chrome", - "translation": "Wersja 100+" + "translation": "Wersja 106+" }, { "id": "web.error.unsupported_browser.learn_more", @@ -9542,5 +9542,29 @@ { "id": "app.job.error", "translation": "Błąd podczas wykonywania zadania." + }, + { + "id": "app.last_accessible_file.app_error", + "translation": "Błąd pobierania ostatniego dostępnego pliku" + }, + { + "id": "app.file.cloud.get.app_error", + "translation": "Nie można pobrać pliku, ponieważ przekroczony został limit planu chmury." + }, + { + "id": "model.group.name.reserved_name.app_error", + "translation": "nazwa grupy już istnieje jako nazwa zastrzeżona" + }, + { + "id": "app.plugin.product_mode.app_error", + "translation": "Wtyczka {{.Name}} nie może być włączona w trybie produktu." + }, + { + "id": "api.team.invite_guests_to_channels.license.error", + "translation": "Twoja licencja nie wspiera kont gości" + }, + { + "id": "api.team.invite_guests_to_channels.disabled.error", + "translation": "Konta gości są wyłączone" } ] From 62152212ccf4bc013b1c3954ab84449f8248743d Mon Sep 17 00:00:00 2001 From: Dmitriy Danilov Date: Wed, 19 Oct 2022 06:20:57 +0200 Subject: [PATCH 49/63] Translated using Weblate (Ukrainian) Currently translated at 70.7% (1687 of 2385 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/uk/ --- i18n/uk.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/i18n/uk.json b/i18n/uk.json index 21f427b84f..4632894522 100644 --- a/i18n/uk.json +++ b/i18n/uk.json @@ -6858,5 +6858,17 @@ { "id": "api.admin.add_certificate.parseform.app_error", "translation": "Помилка синтаксісу багато-форматного запиту" + }, + { + "id": "api.channel.patch_channel_moderations.cache_invalidation.error", + "translation": "Помилка валідації кешу" + }, + { + "id": "api.back_to_app", + "translation": "Повернутися до {{.SiteName}}" + }, + { + "id": "Channels", + "translation": "Канали" } ] From 4c1bcc9e3b9dde8ef94bf5d481098c0659a131d6 Mon Sep 17 00:00:00 2001 From: Matthew Williams Date: Wed, 19 Oct 2022 06:20:58 +0200 Subject: [PATCH 50/63] Translated using Weblate (English (Australia)) Currently translated at 100.0% (2385 of 2385 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/en_AU/ --- i18n/en_AU.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/i18n/en_AU.json b/i18n/en_AU.json index e1dc7cb1c0..026157936b 100644 --- a/i18n/en_AU.json +++ b/i18n/en_AU.json @@ -9537,5 +9537,21 @@ { "id": "app.cloud.get_current_plan_name.app_error", "translation": "Unable to get current plan name" + }, + { + "id": "model.group.name.reserved_name.app_error", + "translation": "group name already exists as a reserved name" + }, + { + "id": "app.last_accessible_file.app_error", + "translation": "Error fetching last accessible file" + }, + { + "id": "app.job.error", + "translation": "Error during job execution." + }, + { + "id": "app.file.cloud.get.app_error", + "translation": "Unable to retrieve file: cloud storage limit exceeded." } ] From b0c39341d6d62ab4ceb563c982caa4e620135b75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20V=C3=ADtor=20Cruz=20de=20Souza?= Date: Wed, 19 Oct 2022 06:20:58 +0200 Subject: [PATCH 51/63] Translated using Weblate (Portuguese (Brazil)) Currently translated at 90.6% (2161 of 2385 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/pt_BR/ Translated using Weblate (Portuguese (Brazil)) Currently translated at 90.5% (2160 of 2385 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/pt_BR/ --- i18n/pt-BR.json | 104 ++++++++++++++++++++++++++---------------------- 1 file changed, 56 insertions(+), 48 deletions(-) diff --git a/i18n/pt-BR.json b/i18n/pt-BR.json index 94b78b3e58..1d35e397f0 100644 --- a/i18n/pt-BR.json +++ b/i18n/pt-BR.json @@ -621,7 +621,7 @@ }, { "id": "api.command_help.desc", - "translation": "Abrir a página de ajuda do Mattermost" + "translation": "Exibir descrição de ajuda do Mattermost" }, { "id": "api.command_help.name", @@ -1173,7 +1173,7 @@ }, { "id": "api.marshal_error", - "translation": "marshal error" + "translation": "Falha para marshal." }, { "id": "api.oauth.allow_oauth.redirect_callback.app_error", @@ -1789,7 +1789,7 @@ }, { "id": "api.templates.email_footer", - "translation": "Para alterar suas preferencias de notificação, faça login no site da sua equipe e vá para Configurações de Conta > Notificações." + "translation": "Para alterar suas preferencias de notificação, faça login no site da sua equipe e vá para Configurações > Notificações." }, { "id": "api.templates.email_info1", @@ -1813,7 +1813,7 @@ }, { "id": "api.templates.invite_body.button", - "translation": "Entrar na Equipe" + "translation": "Entrar agora" }, { "id": "api.templates.invite_body.title", @@ -1857,7 +1857,7 @@ }, { "id": "api.templates.post_body.button", - "translation": "Ir Para a Publicação" + "translation": "Responder no Mattermost" }, { "id": "api.templates.reset_body.button", @@ -1865,7 +1865,7 @@ }, { "id": "api.templates.reset_body.title", - "translation": "Você solicitou uma redefinição de senha" + "translation": "Redefina sua senha" }, { "id": "api.templates.reset_subject", @@ -4333,11 +4333,11 @@ }, { "id": "model.post.is_valid.file_ids.app_error", - "translation": "Ids do arquivo inválido. Observe que os uploads estão limitados a cinco arquivos no máximo. Por favor, use publicações adicionais para mais arquivos." + "translation": "Ids do arquivo inválido. Observe que os uploads estão limitados a 10 arquivos no máximo. Por favor, use publicações adicionais para mais arquivos." }, { "id": "model.post.is_valid.filenames.app_error", - "translation": "Nomes de arquivo inválidos" + "translation": "Nomes de arquivo inválidos." }, { "id": "model.post.is_valid.hashtags.app_error", @@ -4573,7 +4573,7 @@ }, { "id": "oauth.gitlab.tos.error", - "translation": "Os Termos de Serviço do GitLab foram atualizados. Por favor vá até gitlab.com para aceitar os termos e depois tente se logar novamente no Mattermost." + "translation": "Os Termos de Serviço do GitLab foram atualizados. Por favor vá até {{.URL}} para aceitar os termos e depois tente se logar novamente no Mattermost." }, { "id": "plugin.api.update_user_status.bad_status", @@ -4745,19 +4745,19 @@ }, { "id": "web.error.unsupported_browser.min_os_version.windows", - "translation": "Windows 7+" + "translation": "Windows 8.1+" }, { "id": "web.error.unsupported_browser.min_os_version.mac", - "translation": "macOS 10.9+" + "translation": "macOS 10.14+" }, { "id": "web.error.unsupported_browser.min_browser_version.safari", - "translation": "Versão 12+" + "translation": "Versão 14.1+" }, { "id": "web.error.unsupported_browser.min_browser_version.firefox", - "translation": "Versão 60+" + "translation": "Versão 91+" }, { "id": "web.error.unsupported_browser.min_browser_version.edge", @@ -4765,7 +4765,7 @@ }, { "id": "web.error.unsupported_browser.min_browser_version.chrome", - "translation": "Versão 61+" + "translation": "Version 100+" }, { "id": "web.error.unsupported_browser.learn_more", @@ -5945,7 +5945,7 @@ }, { "id": "api.push_notifications.session.expired", - "translation": "Sessão expirada: faça login para continuar recebendo notificações. As sessões para {{.siteName}} estão configuradas pelo Administrador do Sistema para expirar a cada {{.daysCount}} dia(s)." + "translation": "Sessão expirada: faça login para continuar recebendo notificações. As sessões para {{.siteName}} estão configuradas pelo Administrador do Sistema para expirar a cada {{.hoursCount}} hour(s)." }, { "id": "api.post.error_get_post_id.pending", @@ -6189,7 +6189,7 @@ }, { "id": "app.preference.permanent_delete_by_user.app_error", - "translation": "Encontramos um erro enquanto excluía as preferências." + "translation": "Encontramos um erro enquanto excluiamos as preferências." }, { "id": "app.preference.get_category.app_error", @@ -6897,7 +6897,7 @@ }, { "id": "model.upload_session.is_valid.user_id.app_error", - "translation": "Valor inválido para user_id." + "translation": "Valor inválido para UserId" }, { "id": "model.upload_session.is_valid.type.app_error", @@ -6905,15 +6905,15 @@ }, { "id": "model.upload_session.is_valid.path.app_error", - "translation": "Valor inválido para path." + "translation": "Valor inválido para path" }, { "id": "model.upload_session.is_valid.id.app_error", - "translation": "Valor inválido para id." + "translation": "Valor inválido para id" }, { "id": "model.upload_session.is_valid.create_at.app_error", - "translation": "Valor inválido para create_at." + "translation": "Valor inválido para Create_At" }, { "id": "app.upload.upload_data.large_image.app_error", @@ -7601,7 +7601,7 @@ }, { "id": "api.templates.copyright", - "translation": "© 2020 Mattermost, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301" + "translation": "© 2021 Mattermost, Inc. 530 Lytton Avenue, Second floor, Palo Alto, CA, 94301" }, { "id": "api.roles.patch_roles.not_allowed_permission.error", @@ -7645,15 +7645,15 @@ }, { "id": "api.templates.payment_failed.title", - "translation": "Pagamento Falhou" + "translation": "O Pagamento não foi bem sucedido" }, { "id": "api.templates.payment_failed.subject", - "translation": "Ação necessária: Falha no pagamento para Mattermost Cloud" + "translation": "Ação necessária: Falha no pagamento para Mattermost {{.Plan}}" }, { "id": "api.templates.payment_failed.info3", - "translation": "Para garantir a assinatura ininterrupta do Mattermost Cloud, entre em contato com sua instituição financeira para corrigir o problema subjacente ou atualize suas informações de pagamento. Assim que as informações de pagamento forem atualizadas, a Mattermost tentará liquidar o saldo pendente." + "translation": "Para garantir a acesso ininterrupto do Mattermost {{.Plan}}, entre em contato com sua instituição financeira para corrigir o problema subjacente ou atualize suas informações de pagamento. Assim que as informações de pagamento forem atualizadas, a Mattermost tentará liquidar o saldo pendente." }, { "id": "api.templates.payment_failed.info2", @@ -8069,7 +8069,7 @@ }, { "id": "api.user.get_authorization_code.endpoint.app_error", - "translation": "Erro ao recuperar o endpoint da Descoberta de Documento." + "translation": "Erro ao recuperar o endpoint do Documento de Descoberta." }, { "id": "api.templates.welcome_body.subTitle2", @@ -8117,11 +8117,11 @@ }, { "id": "api.templates.questions_footer.info", - "translation": "Envie-nos um email a qualquer momento em " + "translation": "Precisa de ajuda ou tem dúvidas? Envie-nos um e-mail em " }, { "id": "api.templates.email_footer_v2", - "translation": "© 2020 Mattermost, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301" + "translation": "© 2022 Mattermost, Inc. 530 Lytton Avenue, Second floor, Palo Alto, CA, 94301" }, { "id": "api.post.search_files.invalid_body.app_error", @@ -8333,7 +8333,7 @@ }, { "id": "api.context.remote_id_invalid.app_error", - "translation": "Não foi possível encontrar o id do cluster remoto {{.RemoteId}}." + "translation": "Não foi possível encontrar o ID de uma conexão segura {{.RemoteId}}." }, { "id": "api.context.json_encoding.app_error", @@ -8357,15 +8357,15 @@ }, { "id": "api.command_share.remote_uninvited", - "translation": "Remoto `{{.RemoteId}}` sem convite." + "translation": "Conexão segura `{{.RemoteId}}` sem convite." }, { "id": "api.command_share.remote_id_invalid.error", - "translation": "O id do cluster remoto é inválido: {{.Error}}" + "translation": "O ID de conexão segura é inválido: {{.Error}}" }, { "id": "api.command_share.remote_id.help", - "translation": "Id de uma instância remota existente. Veja o comando `remote` para adicionar uma instância remota." + "translation": "ID de uma conexão segura existente. Veja o comando `remote` para adicionar uma conexão segura." }, { "id": "api.command_share.permission_required", @@ -8437,7 +8437,7 @@ }, { "id": "api.templates.cloud_welcome_email.signin_sub_info2", - "translation": "para a melhor experiência em PC, Mac, iOS e Android." + "translation": "para a melhor experiência em Windows, Linux, Mac, iOS e Android." }, { "id": "api.templates.cloud_welcome_email.signin_sub_info", @@ -8469,7 +8469,7 @@ }, { "id": "api.templates.cloud_welcome_email.add_apps_sub_info", - "translation": "Otimize seu trabalho com ferramentas como Github, Google Agenda e Chrome. Explore todas as integrações que temos" + "translation": "Otimize seu trabalho com ferramentas como GitHub, Jira e Zoom. Explore todas as integrações que temos em nosso" }, { "id": "api.templates.cloud_welcome_email.add_apps_info", @@ -8481,11 +8481,11 @@ }, { "id": "api.remote_cluster.update_not_unique.app_error", - "translation": "Já existe um cluster remoto com a mesmo url." + "translation": "Já existe uma conexão segura com a mesmo url." }, { "id": "api.remote_cluster.update.app_error", - "translation": "Encontramos um erro ao atualizar o cluster remoto." + "translation": "Encontramos um erro ao atualizar a conexão segura." }, { "id": "api.remote_cluster.service_not_enabled.app_error", @@ -8493,11 +8493,11 @@ }, { "id": "api.remote_cluster.save_not_unique.app_error", - "translation": "O cluster remoto já foi adicionado." + "translation": "A conexão segura já foi adicionada." }, { "id": "api.remote_cluster.save.app_error", - "translation": "Encontramos um erro ao salvar o cluster remoto." + "translation": "Encontramos um erro ao salvar a conexão segura." }, { "id": "api.remote_cluster.invalid_topic.app_error", @@ -8509,11 +8509,11 @@ }, { "id": "api.remote_cluster.get.app_error", - "translation": "Encontramos um erro ao recuperar um cluster remoto." + "translation": "Encontramos um erro ao recuperar uma conexão segura." }, { "id": "api.remote_cluster.delete.app_error", - "translation": "Encontramos um erro ao excluir o cluster remoto." + "translation": "Encontramos um erro ao excluir uma conexão segura." }, { "id": "api.job.unable_to_create_job.incorrect_job_type", @@ -8533,11 +8533,11 @@ }, { "id": "api.context.remote_id_missing.app_error", - "translation": "Falta a id do cluster remoto." + "translation": "Falta a ID de conexão sergura." }, { "id": "api.context.remote_id_mismatch.app_error", - "translation": "Incompatibilidade de id do cluster remoto." + "translation": "Incompatibilidade de ID da conexão segura." }, { "id": "api.context.invitation_expired.error", @@ -8549,11 +8549,11 @@ }, { "id": "api.command_share.uninvite_remote_id.help", - "translation": "Id da instância remota para desconvidar." + "translation": "ID de uma conexão segura para desconvidar." }, { "id": "api.command_share.uninvite_remote.help", - "translation": "Cancela o convite de uma instância remota deste canal compartilhado" + "translation": "Cancela o convite de uma conexão segura deste canal compartilhado" }, { "id": "api.command_share.shared_channel_unavailable", @@ -8569,11 +8569,11 @@ }, { "id": "api.command_share.remote_not_valid", - "translation": "Deve especificar um cluster remoto válido para cancelar o convite" + "translation": "Deve especificar uma ID de conexão segura válida para cancelar o convite" }, { "id": "api.command_share.remote_already_invited", - "translation": "O cluster remoto já foi convidado." + "translation": "A conexão segura já foi convidada." }, { "id": "api.command_share.not_shared_channel_unshare", @@ -8581,15 +8581,15 @@ }, { "id": "api.command_share.no_remote_invited", - "translation": "Nenhum remoto foi convidado para este canal compartilhado." + "translation": "Nenhuma conexão remota foi convidada a este canal." }, { "id": "api.command_share.name", - "translation": "compartilhar" + "translation": "compartilhar-canal" }, { "id": "api.command_share.must_specify_valid_remote", - "translation": "Deve especificar um ID de conjunto remoto válido para convidar." + "translation": "Deve especificar um ID de conjunto válido seguro para convidar." }, { "id": "api.command_share.missing_action", @@ -8650,5 +8650,13 @@ { "id": "Boards", "translation": "Quadros" + }, + { + "id": "Playbooks", + "translation": "Playbooks" + }, + { + "id": "api.cloud.delinquency_email.missing_email_to_trigger", + "translation": "Campos faltando para envio de email." } ] From 32a941b79dc5d1df2b402e92a668e0cf3bd5997b Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Wed, 19 Oct 2022 06:20:59 +0200 Subject: [PATCH 52/63] Update translation files Updated by "Cleanup translation files" hook in Weblate. Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/ Update translation files Updated by "Cleanup translation files" hook in Weblate. Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/ --- i18n/bg.json | 8 -------- i18n/de.json | 8 -------- i18n/en_AU.json | 8 -------- i18n/es.json | 8 -------- i18n/fa.json | 8 -------- i18n/fr.json | 8 -------- i18n/hu.json | 8 -------- i18n/it.json | 8 -------- i18n/ja.json | 8 -------- i18n/ko.json | 8 -------- i18n/nl.json | 8 -------- i18n/pl.json | 8 -------- i18n/pt-BR.json | 8 -------- i18n/ro.json | 8 -------- i18n/ru.json | 8 -------- i18n/sv.json | 4 ---- i18n/tr.json | 8 -------- i18n/uk.json | 8 -------- i18n/zh-CN.json | 8 -------- i18n/zh-TW.json | 8 -------- 20 files changed, 156 deletions(-) diff --git a/i18n/bg.json b/i18n/bg.json index 7913a2775f..4031bf4bf6 100644 --- a/i18n/bg.json +++ b/i18n/bg.json @@ -4267,14 +4267,6 @@ "id": "api.team.invite_guests.channel_in_invalid_team.app_error", "translation": "Каналите в поканата трябва да са част от канещият екип." }, - { - "id": "api.team.invate_guests_to_channels.license.error", - "translation": "Вашият лиценз не поддържа профили за гости" - }, - { - "id": "api.team.invate_guests_to_channels.disabled.error", - "translation": "Профилите за гости са забранени" - }, { "id": "api.team.invalidate_all_email_invites.app_error", "translation": "Грешка при анулиране на поканите по ел.поща." diff --git a/i18n/de.json b/i18n/de.json index 923ae5eedd..01c6dbcf7f 100644 --- a/i18n/de.json +++ b/i18n/de.json @@ -5623,14 +5623,6 @@ "id": "api.team.invite_guests.channel_in_invalid_team.app_error", "translation": "Die Kanäle der Einladung müssen Teil des Teams der Einladung sein." }, - { - "id": "api.team.invate_guests_to_channels.license.error", - "translation": "Deine Lizenz unterstützt Gastkonten nicht" - }, - { - "id": "api.team.invate_guests_to_channels.disabled.error", - "translation": "Gastkonten sind deaktiviert" - }, { "id": "api.team.invalidate_all_email_invites.app_error", "translation": "Fehler beim Annulieren von E-Mail-Einladungen." diff --git a/i18n/en_AU.json b/i18n/en_AU.json index 026157936b..6f6f177f60 100644 --- a/i18n/en_AU.json +++ b/i18n/en_AU.json @@ -5459,14 +5459,6 @@ "id": "api.team.invite_guests.channel_in_invalid_team.app_error", "translation": "The channels of the invite must be part of the team of the invite." }, - { - "id": "api.team.invate_guests_to_channels.license.error", - "translation": "Your licence does not support guest accounts" - }, - { - "id": "api.team.invate_guests_to_channels.disabled.error", - "translation": "Guest accounts are disabled" - }, { "id": "api.team.invalidate_all_email_invites.app_error", "translation": "Error invalidating email invites." diff --git a/i18n/es.json b/i18n/es.json index 97cec02eba..5a29542a5c 100644 --- a/i18n/es.json +++ b/i18n/es.json @@ -5543,14 +5543,6 @@ "id": "api.team.invite_guests.channel_in_invalid_team.app_error", "translation": "Los canales de la invitación deben ser parte del equipo al cual fue invitado." }, - { - "id": "api.team.invate_guests_to_channels.license.error", - "translation": "La licencia actual no es compatible con cuentas de huéspedes" - }, - { - "id": "api.team.invate_guests_to_channels.disabled.error", - "translation": "Las cuentas de huéspedes están deshabilitadas" - }, { "id": "api.team.invalidate_all_email_invites.app_error", "translation": "Error al invalidar las invitaciones por correo electrónico." diff --git a/i18n/fa.json b/i18n/fa.json index b71235d64b..88186a1d34 100644 --- a/i18n/fa.json +++ b/i18n/fa.json @@ -5159,14 +5159,6 @@ "id": "api.team.invite_guests.channel_in_invalid_team.app_error", "translation": "کانال های دعوت باید بخشی از تیم دعوت باشند." }, - { - "id": "api.team.invate_guests_to_channels.license.error", - "translation": "مجوز شما از حساب های مهمان پشتیبانی نمی کند" - }, - { - "id": "api.team.invate_guests_to_channels.disabled.error", - "translation": "حساب های مهمان غیرفعال شده است" - }, { "id": "api.team.invalidate_all_email_invites.app_error", "translation": "خطا در عدم اعتبار دعوت نامه های ایمیل." diff --git a/i18n/fr.json b/i18n/fr.json index 043cda6743..c94978c9da 100644 --- a/i18n/fr.json +++ b/i18n/fr.json @@ -5583,14 +5583,6 @@ "id": "api.team.invite_guests.channel_in_invalid_team.app_error", "translation": "Les canaux présents dans l'invitation doivent faire partie de la même équipe pour laquelle vous êtes invité." }, - { - "id": "api.team.invate_guests_to_channels.license.error", - "translation": "Votre licence ne prend pas en charge les comptes invités." - }, - { - "id": "api.team.invate_guests_to_channels.disabled.error", - "translation": "Les comptes invités sont désactivés" - }, { "id": "api.team.invalidate_all_email_invites.app_error", "translation": "Une erreur s'est produite lors de l'invalidation des e-mails d'invitation." diff --git a/i18n/hu.json b/i18n/hu.json index 2f7e270a62..930d7fd5d3 100644 --- a/i18n/hu.json +++ b/i18n/hu.json @@ -5795,14 +5795,6 @@ "id": "api.team.invite_guests.channel_in_invalid_team.app_error", "translation": "A meghívás csatornáinak a meghívó csapat részének kell lenniük." }, - { - "id": "api.team.invate_guests_to_channels.license.error", - "translation": "Az Ön licence nem támogatja a vendég fiókokat" - }, - { - "id": "api.team.invate_guests_to_channels.disabled.error", - "translation": "A vendég fiókok le vannak tiltva" - }, { "id": "api.team.invalidate_all_email_invites.app_error", "translation": "Hiba történt az e-mail meghívók érvénytelenítésekor." diff --git a/i18n/it.json b/i18n/it.json index 2b4114bad9..f9c6ae0911 100644 --- a/i18n/it.json +++ b/i18n/it.json @@ -5211,10 +5211,6 @@ "id": "api.team.add_members.user_denied", "translation": "Questa squadra è gestita dai gruppi. Questo utente non appartiene a un gruppo sincronizzato con questa squadra." }, - { - "id": "api.team.invate_guests_to_channels.disabled.error", - "translation": "Gli account ospite sono disattivati" - }, { "id": "api.team.remove_member.group_constrained.app_error", "translation": "Impossibile rimuovere un utente da una squadra con vincoli di gruppo." @@ -5643,10 +5639,6 @@ "id": "api.team.invalidate_all_email_invites.app_error", "translation": "Errore invalidando gli inviti email." }, - { - "id": "api.team.invate_guests_to_channels.license.error", - "translation": "La tua licenza non supporta gli account ospite" - }, { "id": "api.team.invite_guests.channel_in_invalid_team.app_error", "translation": "I canali dell'invito devono far parte della squadra dell'invito." diff --git a/i18n/ja.json b/i18n/ja.json index 5918ca411b..2b9e6beead 100644 --- a/i18n/ja.json +++ b/i18n/ja.json @@ -5579,14 +5579,6 @@ "id": "api.team.invite_guests.channel_in_invalid_team.app_error", "translation": "招待チャンネルは招待チーム内のチャンネルでなくてはなりません。" }, - { - "id": "api.team.invate_guests_to_channels.license.error", - "translation": "現在のライセンスではゲストアカウントはサポートされていません" - }, - { - "id": "api.team.invate_guests_to_channels.disabled.error", - "translation": "ゲストアカウントは無効化されています" - }, { "id": "api.team.invalidate_all_email_invites.app_error", "translation": "電子メール招待の無効化でエラーが発生しました。" diff --git a/i18n/ko.json b/i18n/ko.json index 79e6d396d5..7d4b36ccb3 100644 --- a/i18n/ko.json +++ b/i18n/ko.json @@ -5575,14 +5575,6 @@ "id": "api.team.invite_guests.channel_in_invalid_team.app_error", "translation": "초대 채널은 초대한 팀의 채널이어야 합니다." }, - { - "id": "api.team.invate_guests_to_channels.license.error", - "translation": "게스트 계정을 지원하지 않는 라이선스입니다" - }, - { - "id": "api.team.invate_guests_to_channels.disabled.error", - "translation": "게스트 계정이 비활성화되어 있습니다" - }, { "id": "api.team.invalidate_all_email_invites.app_error", "translation": "전자우편 초대에서 유효하지 않는 오류가 발생했습니다." diff --git a/i18n/nl.json b/i18n/nl.json index 2695bddadb..c23bb81c98 100644 --- a/i18n/nl.json +++ b/i18n/nl.json @@ -5607,14 +5607,6 @@ "id": "api.team.invite_guests.channel_in_invalid_team.app_error", "translation": "De kanalen van de uitgenodigde moet een kanaal dat deel uitmaakt van het team." }, - { - "id": "api.team.invate_guests_to_channels.license.error", - "translation": "Je licentie ondersteunt geen gastgebruikers" - }, - { - "id": "api.team.invate_guests_to_channels.disabled.error", - "translation": "Gast accounts zijn uitgeschakeld" - }, { "id": "api.team.invalidate_all_email_invites.app_error", "translation": "Fout bij afkeuren e-mailuitnodigingen." diff --git a/i18n/pl.json b/i18n/pl.json index 48fe1d9b82..f62b244a3d 100644 --- a/i18n/pl.json +++ b/i18n/pl.json @@ -5583,14 +5583,6 @@ "id": "api.team.invite_guests.channel_in_invalid_team.app_error", "translation": "Kanały do których chcesz zaprosić, muszą być częścią zespołu do którego chcesz zaprosić." }, - { - "id": "api.team.invate_guests_to_channels.license.error", - "translation": "Twoja licencja nie obsługuje kont gości" - }, - { - "id": "api.team.invate_guests_to_channels.disabled.error", - "translation": "Konta gości są wyłączone" - }, { "id": "api.team.invalidate_all_email_invites.app_error", "translation": "Błąd unieważniania zaproszenia E-Mail." diff --git a/i18n/pt-BR.json b/i18n/pt-BR.json index 1d35e397f0..2873fe7a1d 100644 --- a/i18n/pt-BR.json +++ b/i18n/pt-BR.json @@ -5587,14 +5587,6 @@ "id": "api.team.invite_guests.channel_in_invalid_team.app_error", "translation": "Os canais do convite devem ser parte da equipe do convite." }, - { - "id": "api.team.invate_guests_to_channels.license.error", - "translation": "Sua licença não suporta contas convidado" - }, - { - "id": "api.team.invate_guests_to_channels.disabled.error", - "translation": "Contas convidado estão desativadas" - }, { "id": "api.team.invalidate_all_email_invites.app_error", "translation": "Erro ao invalidar convites por email." diff --git a/i18n/ro.json b/i18n/ro.json index c51b4666e0..6c9fe29085 100644 --- a/i18n/ro.json +++ b/i18n/ro.json @@ -5587,14 +5587,6 @@ "id": "api.team.invite_guests.channel_in_invalid_team.app_error", "translation": "Canalele invitației trebuie să facă parte din echipa invitației." }, - { - "id": "api.team.invate_guests_to_channels.license.error", - "translation": "Licența dvs. nu acceptă conturile de invitați" - }, - { - "id": "api.team.invate_guests_to_channels.disabled.error", - "translation": "Conturile oaspeților sunt dezactivate" - }, { "id": "api.team.invalidate_all_email_invites.app_error", "translation": "Eroare la invalidarea invitațiilor de e-mail." diff --git a/i18n/ru.json b/i18n/ru.json index 3d371f6a36..0b4fd7d438 100644 --- a/i18n/ru.json +++ b/i18n/ru.json @@ -5235,10 +5235,6 @@ "id": "api.team.demote_user_to_guest.license.error", "translation": "Ваша лицензия не поддерживает гостевые учётные записи" }, - { - "id": "api.team.invate_guests_to_channels.license.error", - "translation": "Ваша лицензия не поддерживает гостевые учётные записи" - }, { "id": "api.team.remove_member.group_constrained.app_error", "translation": "Невозможно удалить пользователя из управляемой группами команды." @@ -5675,10 +5671,6 @@ "id": "api.team.invalidate_all_email_invites.app_error", "translation": "Ошибка аннулирования e-mail приглашений." }, - { - "id": "api.team.invate_guests_to_channels.disabled.error", - "translation": "Гостевые учётные записи отключены" - }, { "id": "api.team.invite_guests.channel_in_invalid_team.app_error", "translation": "Каналы в приглашении должны быть частью команды в приглашении." diff --git a/i18n/sv.json b/i18n/sv.json index 2374ce62ad..82bd8030ec 100644 --- a/i18n/sv.json +++ b/i18n/sv.json @@ -5275,10 +5275,6 @@ "id": "api.team.invite_guests.channel_in_invalid_team.app_error", "translation": "De kanaler som ingår i inbjudan måste tillhöra de team som inbjudan gäller för." }, - { - "id": "api.team.invate_guests_to_channels.license.error", - "translation": "Din licens tillåter inte gäståtkomst" - }, { "id": "api.team.invate_guests_to_channels.disabled.error", "translation": "Gäståtkomst har inaktiverats" diff --git a/i18n/tr.json b/i18n/tr.json index bdb2ca0902..cbe04ebdb2 100644 --- a/i18n/tr.json +++ b/i18n/tr.json @@ -5655,14 +5655,6 @@ "id": "api.team.invalidate_all_email_invites.app_error", "translation": "E-posta çağrıları geçersiz kılınırken sorun çıktı." }, - { - "id": "api.team.invate_guests_to_channels.disabled.error", - "translation": "Konuk hesapları kullanılmıyor" - }, - { - "id": "api.team.invate_guests_to_channels.license.error", - "translation": "Lisansınızda konuk hesaplarını kullanma özelliği yok" - }, { "id": "api.team.invite_guests.channel_in_invalid_team.app_error", "translation": "Çağrılan kanallar çağrı yapılan takımın bir parçası olmalıdır." diff --git a/i18n/uk.json b/i18n/uk.json index 4632894522..c05c7895b3 100644 --- a/i18n/uk.json +++ b/i18n/uk.json @@ -5583,14 +5583,6 @@ "id": "api.team.invite_guests.channel_in_invalid_team.app_error", "translation": "The channels of the invite must be part of the team of the invite." }, - { - "id": "api.team.invate_guests_to_channels.license.error", - "translation": "Редакція не підтримує Elasticsearch. " - }, - { - "id": "api.team.invate_guests_to_channels.disabled.error", - "translation": "Guest accounts are disabled" - }, { "id": "api.team.invalidate_all_email_invites.app_error", "translation": "Error invalidating email invites." diff --git a/i18n/zh-CN.json b/i18n/zh-CN.json index 4dba1375ff..cecf6c9252 100644 --- a/i18n/zh-CN.json +++ b/i18n/zh-CN.json @@ -5611,10 +5611,6 @@ "id": "api.team.invalidate_all_email_invites.app_error", "translation": "作废邮件邀请错误。" }, - { - "id": "api.team.invate_guests_to_channels.license.error", - "translation": "您的许可证不支持访客帐号" - }, { "id": "api.team.remove_member.group_constrained.app_error", "translation": "无法从组受限团队移除用户。" @@ -5627,10 +5623,6 @@ "id": "api.team.search_teams.pagination_not_implemented.public_team_search", "translation": "仅搜索公共团队不支持分页。" }, - { - "id": "api.team.invate_guests_to_channels.disabled.error", - "translation": "访客帐号已停用" - }, { "id": "api.templates.remove_expired_license.subject", "translation": "Mattermost 企业许可证已停用。" diff --git a/i18n/zh-TW.json b/i18n/zh-TW.json index 0f3fda947e..11b8fcb9ef 100644 --- a/i18n/zh-TW.json +++ b/i18n/zh-TW.json @@ -5579,14 +5579,6 @@ "id": "api.team.invite_guests.channel_in_invalid_team.app_error", "translation": "邀請的頻道必須屬於邀請的團隊" }, - { - "id": "api.team.invate_guests_to_channels.license.error", - "translation": "授權不支援訪客帳號" - }, - { - "id": "api.team.invate_guests_to_channels.disabled.error", - "translation": "訪客帳號已停用" - }, { "id": "api.team.invalidate_all_email_invites.app_error", "translation": "使電子郵件邀請無效時失敗。" From b9bbe5c447613944e7d1b307ad77da8037f00255 Mon Sep 17 00:00:00 2001 From: MArtin Johnson Date: Wed, 19 Oct 2022 06:20:59 +0200 Subject: [PATCH 53/63] Translated using Weblate (Swedish) Currently translated at 100.0% (2385 of 2385 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/sv/ --- i18n/sv.json | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/i18n/sv.json b/i18n/sv.json index 82bd8030ec..290ff4f7e5 100644 --- a/i18n/sv.json +++ b/i18n/sv.json @@ -5275,10 +5275,6 @@ "id": "api.team.invite_guests.channel_in_invalid_team.app_error", "translation": "De kanaler som ingår i inbjudan måste tillhöra de team som inbjudan gäller för." }, - { - "id": "api.team.invate_guests_to_channels.disabled.error", - "translation": "Gäståtkomst har inaktiverats" - }, { "id": "api.team.invalidate_all_email_invites.app_error", "translation": "Fel vid ogiltiggörande av e-postinbjudan." @@ -9537,5 +9533,25 @@ { "id": "app.job.error", "translation": "Fel under utförandet av jobbet." + }, + { + "id": "model.group.name.reserved_name.app_error", + "translation": "gruppnamnet finns redan som ett reserverat namn" + }, + { + "id": "app.last_accessible_file.app_error", + "translation": "Fel vid hämtning av den senast tillgängliga filen" + }, + { + "id": "app.file.cloud.get.app_error", + "translation": "Ditt abonnemang begränsar möjligheten att hämta filen." + }, + { + "id": "api.team.invite_guests_to_channels.license.error", + "translation": "Din licens tillåter inte gäståtkomst" + }, + { + "id": "api.team.invite_guests_to_channels.disabled.error", + "translation": "Gäståtkomst har inaktiverats" } ] From ee718b04a7317352712231dd03117d1f811046cd Mon Sep 17 00:00:00 2001 From: JtheBAB Date: Wed, 19 Oct 2022 06:21:00 +0200 Subject: [PATCH 54/63] Translated using Weblate (German) Currently translated at 100.0% (2386 of 2386 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/de/ Translated using Weblate (German) Currently translated at 100.0% (2386 of 2386 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/de/ --- i18n/de.json | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/i18n/de.json b/i18n/de.json index 01c6dbcf7f..f39f0d304e 100644 --- a/i18n/de.json +++ b/i18n/de.json @@ -4805,7 +4805,7 @@ }, { "id": "web.error.unsupported_browser.min_browser_version.chrome", - "translation": "Version 102+" + "translation": "Version 106+" }, { "id": "web.error.unsupported_browser.learn_more", @@ -9545,5 +9545,17 @@ { "id": "model.group.name.reserved_name.app_error", "translation": "Gruppenname existiert bereits als reservierter Name" + }, + { + "id": "app.plugin.product_mode.app_error", + "translation": "Plugin {{.Name}} kann im Produktmodus nicht aktiviert werden." + }, + { + "id": "api.team.invite_guests_to_channels.license.error", + "translation": "Deine Lizenz unterstützt Gastkonten nicht" + }, + { + "id": "api.team.invite_guests_to_channels.disabled.error", + "translation": "Gastkonten sind deaktiviert" } ] From 78ad750665371231d9e6d1a869fe713d4361290b Mon Sep 17 00:00:00 2001 From: Angel Mendez Cano Date: Wed, 19 Oct 2022 06:21:00 +0200 Subject: [PATCH 55/63] Translated using Weblate (Spanish) Currently translated at 99.7% (2381 of 2386 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/es/ Translated using Weblate (Spanish) Currently translated at 98.2% (2344 of 2386 strings) Translation: mattermost-languages-shipped/mattermost-server Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-server_master/es/ --- i18n/es.json | 338 +++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 317 insertions(+), 21 deletions(-) diff --git a/i18n/es.json b/i18n/es.json index 5a29542a5c..91e6c8d027 100644 --- a/i18n/es.json +++ b/i18n/es.json @@ -621,7 +621,7 @@ }, { "id": "api.command_help.desc", - "translation": "Abrir la página de ayuda de Mattermost" + "translation": "Muestra el mensaje de ayuda de Mattermost" }, { "id": "api.command_help.name", @@ -1781,7 +1781,7 @@ }, { "id": "api.templates.email_change_verify_body.title", - "translation": "Haz actualizado tu correo electrónico" + "translation": "Haz actualizado satisfactoriamente tu correo electrónico" }, { "id": "api.templates.email_change_verify_subject", @@ -1857,7 +1857,7 @@ }, { "id": "api.templates.post_body.button", - "translation": "Ver mensaje" + "translation": "Responder en Mattermost" }, { "id": "api.templates.reset_body.button", @@ -3305,7 +3305,7 @@ }, { "id": "ent.migration.migratetoldap.duplicate_field", - "translation": "No se pueden migrar los usuarios de AD/LDAP con el campo especificado. Se ha detectado un entrada. Por favor, retire todos los duplicados e inténtalo de nuevo." + "translation": "No se pueden migrar los usuarios de AD/LDAP con el campo especificado. Se ha detectado un entrada duplicada. Por favor, elimina todos los duplicados e inténtalo de nuevo." }, { "id": "ent.migration.migratetoldap.user_not_found", @@ -4573,7 +4573,7 @@ }, { "id": "oauth.gitlab.tos.error", - "translation": "Los Términos de Servicio de GitLab se han actualizado. Por favor, vaya a gitlab.com a aceptar y, a continuación, intente iniciar sesión en Mattermost de nuevo." + "translation": "Los Términos de Servicio de GitLab se han actualizado. Por favor, visita {{.URL}} para aceptarlos y, a continuación, intenta iniciar sesión en Mattermost nuevamente." }, { "id": "plugin.api.update_user_status.bad_status", @@ -4753,7 +4753,7 @@ }, { "id": "web.error.unsupported_browser.min_os_version.windows", - "translation": "Windows 7+" + "translation": "Windows 8.1+" }, { "id": "web.error.unsupported_browser.min_os_version.mac", @@ -4761,19 +4761,19 @@ }, { "id": "web.error.unsupported_browser.min_browser_version.safari", - "translation": "Versión 12+" + "translation": "Versión 14.1+" }, { "id": "web.error.unsupported_browser.min_browser_version.firefox", - "translation": "Versión 78+" + "translation": "Versión 91+" }, { "id": "web.error.unsupported_browser.min_browser_version.edge", - "translation": "Versión 44+" + "translation": "Versión 95+" }, { "id": "web.error.unsupported_browser.min_browser_version.chrome", - "translation": "Versión 89+" + "translation": "Versión 106+" }, { "id": "web.error.unsupported_browser.learn_more", @@ -5937,7 +5937,7 @@ }, { "id": "api.push_notifications.session.expired", - "translation": "Sesión caducada: Inicie sesión para continuar recibiendo notificaciones. El administrador del sistema configura las sesiones para {{.siteName}} para que caduquen cada {{.daysCount}} día (s)." + "translation": "Sesión vencida: Inicie sesión para continuar recibiendo notificaciones. El administrador del sistema configura las sesiones para {{.siteName}} para que caduquen cada {{.hoursCount}} hora(s)." }, { "id": "api.post.error_get_post_id.pending", @@ -7613,7 +7613,7 @@ }, { "id": "api.user.get_authorization_code.endpoint.app_error", - "translation": "Error all obtener el endpoint para Documento de Descubrimiento." + "translation": "Error al obtener el endpoint desde Documento de Descubrimiento." }, { "id": "api.templates.payment_failed_no_card.title", @@ -7637,15 +7637,15 @@ }, { "id": "api.templates.payment_failed.title", - "translation": "Pago fallido" + "translation": "El pago ha fallado" }, { "id": "api.templates.payment_failed.subject", - "translation": "Se requiere una acción: El pago de Mattermost Cloud no se ha realizado" + "translation": "Se requiere una acción: El pago de {{.Plan}} Mattermost Cloud no se ha realizado" }, { "id": "api.templates.payment_failed.info3", - "translation": "Para asegurar una suscripción ininterrumpida a Mattermost Cloud, por favor, póngase en contacto con su institución financiera para arreglar el problema subyacente o actualizar su información de pago. Una vez actualizada la información de pago, Mattermost intentará liquidar cualquier saldo pendiente." + "translation": "Para asegurar un acceso ininterrumpido a {{.Plan}} Mattermost Cloud, por favor, póngase en contacto con su institución financiera para resolver el problema subyacente o actualizar su información de pago. Una vez actualizada la información de pago, Mattermost intentará liquidar cualquier saldo pendiente." }, { "id": "api.templates.payment_failed.info2", @@ -7973,7 +7973,7 @@ }, { "id": "api.templates.questions_footer.info", - "translation": "Envíenos un correo electrónico en cualquier momento a " + "translation": "¿Necesitas ayuda o tienes alguna pregunta? Envíanos un correo electrónico a " }, { "id": "api.templates.license_up_for_renewal_title", @@ -8017,11 +8017,11 @@ }, { "id": "api.templates.email_footer_v2", - "translation": "© 2021 Mattermost, Inc. 530 Lytton Avenue, Second floor, Palo Alto, CA, 94301" + "translation": "© 2022 Mattermost, Inc. 530 Lytton Avenue, Second floor, Palo Alto, CA, 94301" }, { "id": "api.templates.cloud_welcome_email.title", - "translation": "¡El período de prueba de 14 días de tu espacio de trabajo {{.WorkSpace}} está listo!" + "translation": "¡Tu espacio de trabajo ya está listo para usar!" }, { "id": "api.templates.cloud_welcome_email.subtitle_info", @@ -8081,7 +8081,7 @@ }, { "id": "api.templates.cloud_welcome_email.add_apps_sub_info", - "translation": "Agiliza tu trabajo con herramientas como Github, Jira y Zoom. Explora todas las integraciones que tenemos en nuestro" + "translation": "Agiliza tu trabajo con herramientas como GitHub, Jira y Zoom. Explora todas las integraciones que tenemos en nuestro" }, { "id": "api.command_share.unknown_action", @@ -9145,7 +9145,7 @@ }, { "id": "api.templates.invite_team_and_channel_subject", - "translation": " " + "translation": "[{{ .SiteName }}] {{ .SenderName }} te invitó a unirte al Canal {{ .ChannelName }} en el Equipo {{ .TeamDisplayName }}" }, { "id": "api.templates.invite_team_and_channels_body.title", @@ -9229,7 +9229,7 @@ }, { "id": "api.templates.invite_team_and_channel_body.title", - "translation": " " + "translation": "{{ .SenderName }} te invitó a unirte al Canal {{ .ChannelName }} en el Equipo {{ .TeamDisplayName}}" }, { "id": "api.templates.server_inactivity_info_bullet", @@ -9262,5 +9262,301 @@ { "id": "api.user.authorize_oauth_user.saml_response_too_long.app_error", "translation": " " + }, + { + "id": "app.cloud.get_subscription_delinquency_date.app_error", + "translation": "La suscripción no es morosa" + }, + { + "id": "app.cloud.get_subscription.app_error", + "translation": "No se pudo recuperar la suscripción de la nube" + }, + { + "id": "app.cloud.get_current_plan_name.app_error", + "translation": "No se puede obtener el nombre del plan actual" + }, + { + "id": "app.cloud.get_cloud_products.app_error", + "translation": "No se pudieron obtener los productos de la nube" + }, + { + "id": "api.templates.delinquency_90.title", + "translation": "Tu workspace Mattermost ha sido degradado" + }, + { + "id": "api.templates.delinquency_90.subtitle3", + "translation": "Para desarchivar tus datos y mantener las características de pago, actualiza tu información de pago." + }, + { + "id": "api.templates.delinquency_90.subtitle2", + "translation": "Además, tus datos pueden haber sido archivados debido a las limitaciones de Cloud Starter." + }, + { + "id": "api.templates.delinquency_90.subtitle1", + "translation": "Si usas características Cloud Professional o Enterprise para operaciones importantes de negocio, ya no estarán activas y experimentarás un rendimiento degradado." + }, + { + "id": "api.templates.delinquency_90.subject", + "translation": "Tu workspace Mattermost Cloud ha sido degradado" + }, + { + "id": "api.templates.delinquency_90.secondary_action_button", + "translation": "Ver Planes y Precios" + }, + { + "id": "api.templates.delinquency_90.button", + "translation": "Actualizar pago" + }, + { + "id": "api.templates.delinquency_75.title", + "translation": "Tu workspace será degradado en 15 días" + }, + { + "id": "api.templates.delinquency_75.subtitle3", + "translation": "Actualiza ahora tu información de pago, o degrada a Cloud Starter." + }, + { + "id": "api.templates.delinquency_75.subtitle2", + "translation": "Tu workspace será degradado a Cloud Starter. Las características de tu {{.Plan}} serán bloqueadas y algunos de los datos de tu workspace podrían ser archivados hasta que liquides completamente tu saldo pendiente." + }, + { + "id": "api.templates.delinquency_75.subtitle1", + "translation": "Este es un aviso final de que no hemos recibido el pago por tu workspace Mattermost Cloud desde {{.DelinquencyDate}}" + }, + { + "id": "api.templates.delinquency_75.subject", + "translation": "Tu {{.Plan}} Mattermost será degradado en 15 días" + }, + { + "id": "api.templates.delinquency_75.downgrade_to_starter", + "translation": "Degradar a Cloud Starter" + }, + { + "id": "api.templates.delinquency_75.button", + "translation": "Actualizar pago" + }, + { + "id": "api.templates.delinquency_7.title", + "translation": "Tu pago no se completó" + }, + { + "id": "api.templates.delinquency_7.subtitle2", + "translation": "Para mantener tu plan {{.Plan}} activo, por favor contacta a tu institución financiera tan pronto como sea posible, Luego, actualiza tus detalles de pago según sea necesario." + }, + { + "id": "api.templates.delinquency_7.subtitle1", + "translation": "No pudimos procesar tu pago más reciente" + }, + { + "id": "api.templates.delinquency_7.button", + "translation": "Actualizar pago" + }, + { + "id": "api.templates.delinquency_60.title", + "translation": "Tu workspace Mattermost será degradado en 30 días" + }, + { + "id": "api.templates.delinquency_60.subtitle3", + "translation": "Actualiza tu información de pago ahora o degrada a Cloud Starter abajo." + }, + { + "id": "api.templates.delinquency_60.subtitle2", + "translation": "Degradaremos tu workspace automáticamente en 30 días si no somos capaces de procesar tu pago." + }, + { + "id": "api.templates.delinquency_60.subtitle1", + "translation": "Por favor actualiza pronto tu información de pago para procesar tus facturas pendientes." + }, + { + "id": "api.templates.delinquency_60.subject", + "translation": "Acción requerida: El workspace será degradado en 30 días" + }, + { + "id": "api.templates.delinquency_60.downgrade_to_starter", + "translation": "Degradar a Cloud Starter" + }, + { + "id": "api.templates.delinquency_60.button", + "translation": "Actualizar pago" + }, + { + "id": "api.templates.delinquency_45.title", + "translation": "Tu workspace será degradado pronto" + }, + { + "id": "api.templates.delinquency_45.subtitle3", + "translation": "Actualiza la información de tu tarjeta de crédito ahora." + }, + { + "id": "api.templates.delinquency_45.subtitle2", + "translation": "Un workspace degradado podría afectar negativamente los flujos de trabajo críticos, integraciones y otras actividades críticas de negocio realizadas en tu workspace." + }, + { + "id": "api.templates.delinquency_45.subtitle1", + "translation": "No fuimos capaces de cobrar el pago para las facturas con fecha {{.DelinquencyDate}}. Tu workspace está en riesgo de ser degradado." + }, + { + "id": "api.templates.delinquency_45.subject", + "translation": "Aviso: Tu {{.Plan}} Mattermost será degradado pronto" + }, + { + "id": "api.templates.delinquency_45.button", + "translation": "Actualizar pago" + }, + { + "id": "api.templates.delinquency_30.title", + "translation": "Tu workspace será degradado pronto" + }, + { + "id": "api.templates.delinquency_30.subtitle2", + "translation": "si no se toma ninguna medida, tu workspace será degradado y los siguientes datos serán archivados:" + }, + { + "id": "api.templates.delinquency_30.subtitle1", + "translation": "Tienes tiempo para mantener activo tu {{.Plan}} Mattermost pero necesitarás resolver los problemas con tu método de pago." + }, + { + "id": "api.templates.delinquency_30.subject", + "translation": "Actúa para mantener las característica de tu {{.Plan}} Mattermost" + }, + { + "id": "api.templates.delinquency_30.limits_documentation", + "translation": "Ver toda la documentación de límites." + }, + { + "id": "api.templates.delinquency_30.button", + "translation": "Actualizar pago" + }, + { + "id": "api.templates.delinquency_30.bullet.plugins", + "translation": "Plugins e integraciones activas" + }, + { + "id": "api.templates.delinquency_30.bullet.message_history", + "translation": "Historial de mensajes" + }, + { + "id": "api.templates.delinquency_30.bullet.files", + "translation": "Archivos" + }, + { + "id": "api.templates.delinquency_30.bullet.cards", + "translation": "Tarjetas de tus Boards" + }, + { + "id": "api.templates.delinquency_14.title", + "translation": "Pago no recibido" + }, + { + "id": "api.templates.delinquency_14.subtitle2", + "translation": "Por favor contacta a tu institución financiera para resolver cualquier problema. Luego, actualiza los detalles de pago como sean necesarios." + }, + { + "id": "api.templates.delinquency_14.subtitle1", + "translation": "No pudimos realizar el cargo a la tarjeta de crédito que tenemos registrada. Por lo cual tu workspace está en riesgo de ser degradado a Cloud Starter." + }, + { + "id": "api.templates.delinquency_14.subject", + "translation": "El pago está vencido para tu {{.Plan}} Mattermost." + }, + { + "id": "api.templates.delinquency_14.button", + "translation": "Actualizar pago" + }, + { + "id": "api.team.invite_guests_to_channels.license.error", + "translation": "Tu licencia no admite cuentas de invitado" + }, + { + "id": "api.team.invite_guests_to_channels.disabled.error", + "translation": "Las cuentas de invitado están deshabilitadas" + }, + { + "id": "api.command_marketplace.unsupported.app_error", + "translation": "El comando marketplace no es soportado por tu dispositivo." + }, + { + "id": "api.command_marketplace.name", + "translation": "marketplace" + }, + { + "id": "api.command_marketplace.desc", + "translation": "Abre el Marketplace" + }, + { + "id": "api.command_help.success", + "translation": "Mattermost es una plataforma de código abierto para comunicación segura, colaboración, y orquestación de trabajo entre herramientas y equipos.\nMattermost contiene tres herramientas clave:\n\n**Channels** - Mantente conectado con tu equipo vía 1:1 y en por mensajes grupales.\n**[Playbooks](/playbooks)** - Construye y configura procesos repetibles para lograr resultados específicos y predecibles.\n**[Tableros](/boards)** - Administra proyectos y tareas en una estructura de tablero Kanban para ayudar a que tú equipo logre los hitos clave.\n\n[Ver documentación y guías]({{.HelpLink}})" + }, + { + "id": "app.cloud.trial_plan_bot_message", + "translation": "los miembros {{.UsersNum}} del espacio de trabajo {{.WorkspaceName}} han solicitado comenzar la prueba Enterprise para acceder a: " + }, + { + "id": "api.cloud.delinquency_email.missing_email_to_trigger", + "translation": "Campos faltantes requeridos para enviar el correo electrónico de morosidad." + }, + { + "id": "model.group.name.reserved_name.app_error", + "translation": "el nombre de grupo ya existe como un nombre reservado" + }, + { + "id": "model.config.is_valid.amazons3_timeout.app_error", + "translation": "Valor de timeout inválido {{.Value}}. Debe ser un número positivo." + }, + { + "id": "ent.saml.configure.certificate_parse_error.app_error", + "translation": "SAML no pudo cargar satisfactoriamente el Identity Provider Public Certificate, por favor contacta a tu administrador del sistema." + }, + { + "id": "app.user.get_badge_count.app_error", + "translation": "No pudimos obtener el recuento de insignias para el usuario." + }, + { + "id": "app.team.clear_cache.app_error", + "translation": "Error al limpiar el caché de los miembros del equipo" + }, + { + "id": "app.post_reminder_dm", + "translation": "Hola, este es un recordatorio acerca de este mensaje de @{{.Username}}: {{.SiteURL}}/{{.TeamName}}/pl/{{.PostId}}" + }, + { + "id": "app.post.get_top_dms_for_user_since.app_error", + "translation": "No es posible obtener los DMs top para el usuario." + }, + { + "id": "app.plugin.product_mode.app_error", + "translation": "El plugin {{.Name}} no se pudo activar en el modo producto." + }, + { + "id": "app.notify_admin.send_notification_post.app_error", + "translation": "No es posible enviar la publicación de notificación." + }, + { + "id": "app.notify_admin.save.app_error", + "translation": "No es posible guardar los datos de notificación." + }, + { + "id": "app.last_accessible_file.app_error", + "translation": "Error al obtener el último archivo accesible" + }, + { + "id": "app.job.error", + "translation": "Error durante la ejecución de un job." + }, + { + "id": "app.file.cloud.get.app_error", + "translation": "No se puede obtener el archivo porque superó los límites del plan en la nube." + }, + { + "id": "app.cloud.upgrade_plan_bot_message_single", + "translation": "el miembro {{.UsersNum}} del workspace {{.WorkspaceName}} ha solicitado una actualización del workspace para: " + }, + { + "id": "app.cloud.upgrade_plan_bot_message", + "translation": "los miembros {{.UsersNum}} del workspace {{.WorkspaceName}} han solicitado una actualización del workspace para: " + }, + { + "id": "app.cloud.trial_plan_bot_message_single", + "translation": "el miembro {{.UsersNum}} del workspace {{.WorkspaceName}} ha solicitado comenzar la prueba Enterprise para acceder a: " } ] From 312bf283a8c577189fb7f643af5e68977b561cab Mon Sep 17 00:00:00 2001 From: Shivashis Padhi Date: Thu, 20 Oct 2022 10:08:09 +0530 Subject: [PATCH 56/63] [MM-47538] Pass handler functions directly while creating API endpoints (#21441) * Pass handler functions directly without permission check middlewares * Create necessary handlers in api4/group_local.go * Refactor GetGroupsByTeam, GetGroupsByChannel to a common function for local and API --- api4/group.go | 241 +++++++++++++++++++++++++++++++------------- api4/group_local.go | 36 ++++++- api4/handlers.go | 38 +++---- api4/insights.go | 141 ++++++++++++++++++++++++-- 4 files changed, 351 insertions(+), 105 deletions(-) diff --git a/api4/group.go b/api4/group.go index f8b65aa5bb..abcde27459 100644 --- a/api4/group.go +++ b/api4/group.go @@ -18,82 +18,88 @@ import ( func (api *API) InitGroup() { // GET /api/v4/groups - api.BaseRoutes.Groups.Handle("", api.APISessionRequired(requireLicense(getGroups))).Methods("GET") + api.BaseRoutes.Groups.Handle("", api.APISessionRequired(getGroups)).Methods("GET") // POST /api/v4/groups - api.BaseRoutes.Groups.Handle("", api.APISessionRequired(requireLicense(createGroup))).Methods("POST") + api.BaseRoutes.Groups.Handle("", api.APISessionRequired(createGroup)).Methods("POST") // GET /api/v4/groups/:group_id api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}", - api.APISessionRequired(requireLicense(getGroup))).Methods("GET") + api.APISessionRequired(getGroup)).Methods("GET") // PUT /api/v4/groups/:group_id/patch api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/patch", - api.APISessionRequired(requireLicense(patchGroup))).Methods("PUT") + api.APISessionRequired(patchGroup)).Methods("PUT") // POST /api/v4/groups/:group_id/teams/:team_id/link // POST /api/v4/groups/:group_id/channels/:channel_id/link api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/{syncable_type:teams|channels}/{syncable_id:[A-Za-z0-9]+}/link", - api.APISessionRequired(requireLicense(linkGroupSyncable))).Methods("POST") + api.APISessionRequired(linkGroupSyncable)).Methods("POST") // DELETE /api/v4/groups/:group_id/teams/:team_id/link // DELETE /api/v4/groups/:group_id/channels/:channel_id/link api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/{syncable_type:teams|channels}/{syncable_id:[A-Za-z0-9]+}/link", - api.APISessionRequired(requireLicense(unlinkGroupSyncable))).Methods("DELETE") + api.APISessionRequired(unlinkGroupSyncable)).Methods("DELETE") // GET /api/v4/groups/:group_id/teams/:team_id // GET /api/v4/groups/:group_id/channels/:channel_id api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/{syncable_type:teams|channels}/{syncable_id:[A-Za-z0-9]+}", - api.APISessionRequired(requireLicense(getGroupSyncable))).Methods("GET") + api.APISessionRequired(getGroupSyncable)).Methods("GET") // GET /api/v4/groups/:group_id/teams // GET /api/v4/groups/:group_id/channels api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/{syncable_type:teams|channels}", - api.APISessionRequired(requireLicense(getGroupSyncables))).Methods("GET") + api.APISessionRequired(getGroupSyncables)).Methods("GET") // PUT /api/v4/groups/:group_id/teams/:team_id/patch // PUT /api/v4/groups/:group_id/channels/:channel_id/patch api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/{syncable_type:teams|channels}/{syncable_id:[A-Za-z0-9]+}/patch", - api.APISessionRequired(requireLicense(patchGroupSyncable))).Methods("PUT") + api.APISessionRequired(patchGroupSyncable)).Methods("PUT") // GET /api/v4/groups/:group_id/stats api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/stats", - api.APISessionRequired(requireLicense(getGroupStats))).Methods("GET") + api.APISessionRequired(getGroupStats)).Methods("GET") // GET /api/v4/groups/:group_id/members api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/members", - api.APISessionRequired(requireLicense(getGroupMembers))).Methods("GET") + api.APISessionRequired(getGroupMembers)).Methods("GET") // GET /api/v4/users/:user_id/groups api.BaseRoutes.Users.Handle("/{user_id:[A-Za-z0-9]+}/groups", - api.APISessionRequired(requireLicense(getGroupsByUserId))).Methods("GET") + api.APISessionRequired(getGroupsByUserId)).Methods("GET") // GET /api/v4/channels/:channel_id/groups api.BaseRoutes.Channels.Handle("/{channel_id:[A-Za-z0-9]+}/groups", - api.APISessionRequired(requireLicense(getGroupsByChannel))).Methods("GET") + api.APISessionRequired(getGroupsByChannel)).Methods("GET") // GET /api/v4/teams/:team_id/groups api.BaseRoutes.Teams.Handle("/{team_id:[A-Za-z0-9]+}/groups", - api.APISessionRequired(requireLicense(getGroupsByTeam))).Methods("GET") + api.APISessionRequired(getGroupsByTeam)).Methods("GET") // GET /api/v4/teams/:team_id/groups_by_channels api.BaseRoutes.Teams.Handle("/{team_id:[A-Za-z0-9]+}/groups_by_channels", - api.APISessionRequired(requireLicense(getGroupsAssociatedToChannelsByTeam))).Methods("GET") + api.APISessionRequired(getGroupsAssociatedToChannelsByTeam)).Methods("GET") // DELETE /api/v4/groups/:group_id api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}", - api.APISessionRequired(requireLicense(deleteGroup))).Methods("DELETE") + api.APISessionRequired(deleteGroup)).Methods("DELETE") // POST /api/v4/groups/:group_id/members api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/members", - api.APISessionRequired(requireLicense(addGroupMembers))).Methods("POST") + api.APISessionRequired(addGroupMembers)).Methods("POST") // DELETE /api/v4/groups/:group_id/members api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/members", - api.APISessionRequired(requireLicense(deleteGroupMembers))).Methods("DELETE") + api.APISessionRequired(deleteGroupMembers)).Methods("DELETE") } func getGroup(c *Context, w http.ResponseWriter, r *http.Request) { + permissionErr := requireLicense(c) + if permissionErr != nil { + c.Err = permissionErr + return + } + c.RequireGroupId() if c.Err != nil { return @@ -130,6 +136,11 @@ func getGroup(c *Context, w http.ResponseWriter, r *http.Request) { } func createGroup(c *Context, w http.ResponseWriter, r *http.Request) { + permissionErr := requireLicense(c) + if permissionErr != nil { + c.Err = permissionErr + return + } var group *model.GroupWithUserIds if err := json.NewDecoder(r.Body).Decode(&group); err != nil { c.SetInvalidParamWithErr("group", err) @@ -185,6 +196,11 @@ func createGroup(c *Context, w http.ResponseWriter, r *http.Request) { } func patchGroup(c *Context, w http.ResponseWriter, r *http.Request) { + permissionErr := requireLicense(c) + if permissionErr != nil { + c.Err = permissionErr + return + } c.RequireGroupId() if c.Err != nil { return @@ -277,6 +293,11 @@ func patchGroup(c *Context, w http.ResponseWriter, r *http.Request) { } func linkGroupSyncable(c *Context, w http.ResponseWriter, r *http.Request) { + permissionErr := requireLicense(c) + if permissionErr != nil { + c.Err = permissionErr + return + } c.RequireGroupId() if c.Err != nil { return @@ -368,6 +389,11 @@ func linkGroupSyncable(c *Context, w http.ResponseWriter, r *http.Request) { } func getGroupSyncable(c *Context, w http.ResponseWriter, r *http.Request) { + permissionErr := requireLicense(c) + if permissionErr != nil { + c.Err = permissionErr + return + } c.RequireGroupId() if c.Err != nil { return @@ -411,6 +437,11 @@ func getGroupSyncable(c *Context, w http.ResponseWriter, r *http.Request) { } func getGroupSyncables(c *Context, w http.ResponseWriter, r *http.Request) { + permissionErr := requireLicense(c) + if permissionErr != nil { + c.Err = permissionErr + return + } c.RequireGroupId() if c.Err != nil { return @@ -448,6 +479,11 @@ func getGroupSyncables(c *Context, w http.ResponseWriter, r *http.Request) { } func patchGroupSyncable(c *Context, w http.ResponseWriter, r *http.Request) { + permissionErr := requireLicense(c) + if permissionErr != nil { + c.Err = permissionErr + return + } c.RequireGroupId() if c.Err != nil { return @@ -529,6 +565,11 @@ func patchGroupSyncable(c *Context, w http.ResponseWriter, r *http.Request) { } func unlinkGroupSyncable(c *Context, w http.ResponseWriter, r *http.Request) { + permissionErr := requireLicense(c) + if permissionErr != nil { + c.Err = permissionErr + return + } c.RequireGroupId() if c.Err != nil { return @@ -606,6 +647,11 @@ func verifyLinkUnlinkPermission(c *Context, syncableType model.GroupSyncableType } func getGroupMembers(c *Context, w http.ResponseWriter, r *http.Request) { + permissionErr := requireLicense(c) + if permissionErr != nil { + c.Err = permissionErr + return + } c.RequireGroupId() if c.Err != nil { return @@ -651,6 +697,11 @@ func getGroupMembers(c *Context, w http.ResponseWriter, r *http.Request) { } func getGroupStats(c *Context, w http.ResponseWriter, r *http.Request) { + permissionErr := requireLicense(c) + if permissionErr != nil { + c.Err = permissionErr + return + } c.RequireGroupId() if c.Err != nil { return @@ -686,6 +737,11 @@ func getGroupStats(c *Context, w http.ResponseWriter, r *http.Request) { } func getGroupsByUserId(c *Context, w http.ResponseWriter, r *http.Request) { + permissionErr := requireLicense(c) + if permissionErr != nil { + c.Err = permissionErr + return + } c.RequireUserId() if c.Err != nil { return @@ -717,72 +773,46 @@ func getGroupsByUserId(c *Context, w http.ResponseWriter, r *http.Request) { } func getGroupsByChannel(c *Context, w http.ResponseWriter, r *http.Request) { + permissionErr := requireLicense(c) + if permissionErr != nil { + c.Err = permissionErr + return + } c.RequireChannelId() if c.Err != nil { return } - - if c.App.Channels().License() == nil || !*c.App.Channels().License().Features.LDAPGroups { - c.Err = model.NewAppError("Api4.getGroupsByChannel", "api.ldap_groups.license_error", nil, "", http.StatusForbidden) - return - } - - channel, appErr := c.App.GetChannel(c.AppContext, c.Params.ChannelId) + b, appErr := getGroupsByChannelCommon(c, r) if appErr != nil { c.Err = appErr return } - - var permission *model.Permission - if channel.Type == model.ChannelTypePrivate { - permission = model.PermissionReadPrivateChannelGroups - } else { - permission = model.PermissionReadPublicChannelGroups - } - if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, permission) { - c.SetPermissionError(permission) - return - } - - opts := model.GroupSearchOpts{ - Q: c.Params.Q, - IncludeMemberCount: c.Params.IncludeMemberCount, - FilterAllowReference: c.Params.FilterAllowReference, - } - if c.Params.Paginate == nil || *c.Params.Paginate { - opts.PageOpts = &model.PageOpts{Page: c.Params.Page, PerPage: c.Params.PerPage} - } - - groups, totalCount, appErr := c.App.GetGroupsByChannel(c.Params.ChannelId, opts) - if appErr != nil { - c.Err = appErr - return - } - - b, err := json.Marshal(struct { - Groups []*model.GroupWithSchemeAdmin `json:"groups"` - Count int `json:"total_group_count"` - }{ - Groups: groups, - Count: totalCount, - }) - if err != nil { - c.Err = model.NewAppError("Api4.getGroupsByChannel", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err) - return - } - w.Write(b) } func getGroupsByTeam(c *Context, w http.ResponseWriter, r *http.Request) { + permissionErr := requireLicense(c) + if permissionErr != nil { + c.Err = permissionErr + return + } c.RequireTeamId() if c.Err != nil { return } - if c.App.Channels().License() == nil || !*c.App.Channels().License().Features.LDAPGroups { - c.Err = model.NewAppError("Api4.getGroupsByTeam", "api.ldap_groups.license_error", nil, "", http.StatusForbidden) + + b, appError := getGroupsByTeamCommon(c, r) + if appError != nil { + c.Err = appError return } + w.Write(b) +} + +func getGroupsByTeamCommon(c *Context, r *http.Request) ([]byte, *model.AppError) { + if c.App.Channels().License() == nil || !*c.App.Channels().License().Features.LDAPGroups { + return nil, model.NewAppError("Api4.getGroupsByTeam", "api.ldap_groups.license_error", nil, "", http.StatusForbidden) + } opts := model.GroupSearchOpts{ Q: c.Params.Q, @@ -795,8 +825,7 @@ func getGroupsByTeam(c *Context, w http.ResponseWriter, r *http.Request) { groups, totalCount, appErr := c.App.GetGroupsByTeam(c.Params.TeamId, opts) if appErr != nil { - c.Err = appErr - return + return nil, appErr } b, err := json.Marshal(struct { @@ -808,14 +837,64 @@ func getGroupsByTeam(c *Context, w http.ResponseWriter, r *http.Request) { }) if err != nil { - c.Err = model.NewAppError("Api4.getGroupsByTeam", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err) - return + return nil, model.NewAppError("Api4.getGroupsByTeam", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err) } - w.Write(b) + return b, nil +} +func getGroupsByChannelCommon(c *Context, r *http.Request) ([]byte, *model.AppError) { + if c.App.Channels().License() == nil || !*c.App.Channels().License().Features.LDAPGroups { + return nil, model.NewAppError("Api4.getGroupsByChannel", "api.ldap_groups.license_error", nil, "", http.StatusForbidden) + } + + channel, appErr := c.App.GetChannel(c.AppContext, c.Params.ChannelId) + if appErr != nil { + return nil, appErr + } + + var permission *model.Permission + if channel.Type == model.ChannelTypePrivate { + permission = model.PermissionReadPrivateChannelGroups + } else { + permission = model.PermissionReadPublicChannelGroups + } + if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, permission) { + return nil, c.App.MakePermissionError(c.AppContext.Session(), []*model.Permission{permission}) + } + + opts := model.GroupSearchOpts{ + Q: c.Params.Q, + IncludeMemberCount: c.Params.IncludeMemberCount, + FilterAllowReference: c.Params.FilterAllowReference, + } + if c.Params.Paginate == nil || *c.Params.Paginate { + opts.PageOpts = &model.PageOpts{Page: c.Params.Page, PerPage: c.Params.PerPage} + } + + groups, totalCount, appErr := c.App.GetGroupsByChannel(c.Params.ChannelId, opts) + if appErr != nil { + return nil, appErr + } + + b, err := json.Marshal(struct { + Groups []*model.GroupWithSchemeAdmin `json:"groups"` + Count int `json:"total_group_count"` + }{ + Groups: groups, + Count: totalCount, + }) + if err != nil { + return nil, model.NewAppError("Api4.getGroupsByChannel", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err) + } + return b, nil } func getGroupsAssociatedToChannelsByTeam(c *Context, w http.ResponseWriter, r *http.Request) { + permissionErr := requireLicense(c) + if permissionErr != nil { + c.Err = permissionErr + return + } c.RequireTeamId() if c.Err != nil { return @@ -855,6 +934,11 @@ func getGroupsAssociatedToChannelsByTeam(c *Context, w http.ResponseWriter, r *h } func getGroups(c *Context, w http.ResponseWriter, r *http.Request) { + permissionErr := requireLicense(c) + if permissionErr != nil { + c.Err = permissionErr + return + } var teamID, channelID string source := c.Params.GroupSource @@ -961,6 +1045,11 @@ func getGroups(c *Context, w http.ResponseWriter, r *http.Request) { } func deleteGroup(c *Context, w http.ResponseWriter, r *http.Request) { + permissionErr := requireLicense(c) + if permissionErr != nil { + c.Err = permissionErr + return + } c.RequireGroupId() if c.Err != nil { return @@ -1004,6 +1093,11 @@ func deleteGroup(c *Context, w http.ResponseWriter, r *http.Request) { } func addGroupMembers(c *Context, w http.ResponseWriter, r *http.Request) { + permissionErr := requireLicense(c) + if permissionErr != nil { + c.Err = permissionErr + return + } c.RequireGroupId() if c.Err != nil { return @@ -1058,6 +1152,11 @@ func addGroupMembers(c *Context, w http.ResponseWriter, r *http.Request) { } func deleteGroupMembers(c *Context, w http.ResponseWriter, r *http.Request) { + permissionErr := requireLicense(c) + if permissionErr != nil { + c.Err = permissionErr + return + } c.RequireGroupId() if c.Err != nil { return diff --git a/api4/group_local.go b/api4/group_local.go index 5ede4d7bf3..1964fdbed4 100644 --- a/api4/group_local.go +++ b/api4/group_local.go @@ -3,7 +3,39 @@ package api4 +import ( + "net/http" +) + func (api *API) InitGroupLocal() { - api.BaseRoutes.Channels.Handle("/{channel_id:[A-Za-z0-9]+}/groups", api.APILocal(getGroupsByChannel)).Methods("GET") - api.BaseRoutes.Teams.Handle("/{team_id:[A-Za-z0-9]+}/groups", api.APILocal(getGroupsByTeam)).Methods("GET") + api.BaseRoutes.Channels.Handle("/{channel_id:[A-Za-z0-9]+}/groups", api.APILocal(getGroupsByChannelLocal)).Methods("GET") + api.BaseRoutes.Teams.Handle("/{team_id:[A-Za-z0-9]+}/groups", api.APILocal(getGroupsByTeamLocal)).Methods("GET") +} + +func getGroupsByChannelLocal(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequireChannelId() + if c.Err != nil { + return + } + b, appErr := getGroupsByChannelCommon(c, r) + if appErr != nil { + c.Err = appErr + return + } + + w.Write(b) +} + +func getGroupsByTeamLocal(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequireTeamId() + if c.Err != nil { + return + } + b, appError := getGroupsByTeamCommon(c, r) + if appError != nil { + c.Err = appError + return + } + + w.Write(b) } diff --git a/api4/handlers.go b/api4/handlers.go index 9ec6be4aae..250dac32e7 100644 --- a/api4/handlers.go +++ b/api4/handlers.go @@ -200,33 +200,27 @@ func (api *API) APILocal(h handlerFunc) http.Handler { return handler } -func requireLicense(f handlerFunc) handlerFunc { - return func(c *Context, w http.ResponseWriter, r *http.Request) { - if c.App.Channels().License() == nil { - c.Err = model.NewAppError("", "api.license_error", nil, "", http.StatusNotImplemented) - return - } - f(c, w, r) +func requireLicense(c *Context) *model.AppError { + if c.App.Channels().License() == nil { + err := model.NewAppError("", "api.license_error", nil, "", http.StatusNotImplemented) + return err } + return nil } -func minimumProfessionalLicense(f handlerFunc) handlerFunc { - return func(c *Context, w http.ResponseWriter, r *http.Request) { - lic := c.App.Srv().License() - if lic == nil || (lic.SkuShortName != model.LicenseShortSkuProfessional && lic.SkuShortName != model.LicenseShortSkuEnterprise) { - c.Err = model.NewAppError("", model.NoTranslation, nil, "license is neither professional nor enterprise", http.StatusNotImplemented) - return - } - f(c, w, r) +func minimumProfessionalLicense(c *Context) *model.AppError { + lic := c.App.Srv().License() + if lic == nil || (lic.SkuShortName != model.LicenseShortSkuProfessional && lic.SkuShortName != model.LicenseShortSkuEnterprise) { + err := model.NewAppError("", model.NoTranslation, nil, "license is neither professional nor enterprise", http.StatusNotImplemented) + return err } + return nil } -func rejectGuests(f handlerFunc) handlerFunc { - return func(c *Context, w http.ResponseWriter, r *http.Request) { - if c.AppContext.Session().Props[model.SessionPropIsGuest] == "true" { - c.Err = model.NewAppError("", model.NoTranslation, nil, "insufficient permissions as a guest user", http.StatusNotImplemented) - return - } - f(c, w, r) +func rejectGuests(c *Context) *model.AppError { + if c.AppContext.Session().Props[model.SessionPropIsGuest] == "true" { + err := model.NewAppError("", model.NoTranslation, nil, "insufficient permissions as a guest user", http.StatusNotImplemented) + return err } + return nil } diff --git a/api4/insights.go b/api4/insights.go index 3c90294ec7..fce03cfa04 100644 --- a/api4/insights.go +++ b/api4/insights.go @@ -13,31 +13,44 @@ import ( func (api *API) InitInsights() { // Reactions - api.BaseRoutes.InsightsForTeam.Handle("/reactions", api.APISessionRequired(minimumProfessionalLicense(rejectGuests(getTopReactionsForTeamSince)))).Methods("GET") - api.BaseRoutes.InsightsForUser.Handle("/reactions", api.APISessionRequired(minimumProfessionalLicense(rejectGuests(getTopReactionsForUserSince)))).Methods("GET") + api.BaseRoutes.InsightsForTeam.Handle("/reactions", api.APISessionRequired(getTopReactionsForTeamSince)).Methods("GET") + api.BaseRoutes.InsightsForUser.Handle("/reactions", api.APISessionRequired(getTopReactionsForUserSince)).Methods("GET") // Channels - api.BaseRoutes.InsightsForTeam.Handle("/channels", api.APISessionRequired(minimumProfessionalLicense(rejectGuests(getTopChannelsForTeamSince)))).Methods("GET") - api.BaseRoutes.InsightsForUser.Handle("/channels", api.APISessionRequired(minimumProfessionalLicense(rejectGuests(getTopChannelsForUserSince)))).Methods("GET") + api.BaseRoutes.InsightsForTeam.Handle("/channels", api.APISessionRequired(getTopChannelsForTeamSince)).Methods("GET") + api.BaseRoutes.InsightsForUser.Handle("/channels", api.APISessionRequired(getTopChannelsForUserSince)).Methods("GET") // Threads - api.BaseRoutes.InsightsForTeam.Handle("/threads", api.APISessionRequired(minimumProfessionalLicense(rejectGuests(getTopThreadsForTeamSince)))).Methods("GET") - api.BaseRoutes.InsightsForUser.Handle("/threads", api.APISessionRequired(minimumProfessionalLicense(rejectGuests(getTopThreadsForUserSince)))).Methods("GET") + api.BaseRoutes.InsightsForTeam.Handle("/threads", api.APISessionRequired(getTopThreadsForTeamSince)).Methods("GET") + api.BaseRoutes.InsightsForUser.Handle("/threads", api.APISessionRequired(getTopThreadsForUserSince)).Methods("GET") // user DMs - api.BaseRoutes.InsightsForUser.Handle("/dms", api.APISessionRequired(minimumProfessionalLicense(rejectGuests(getTopDMsForUserSince)))).Methods("GET") + api.BaseRoutes.InsightsForUser.Handle("/dms", api.APISessionRequired(getTopDMsForUserSince)).Methods("GET") // Inactive channels - api.BaseRoutes.InsightsForTeam.Handle("/inactive_channels", api.APISessionRequired(minimumProfessionalLicense(rejectGuests(getTopInactiveChannelsForTeamSince)))).Methods("GET") - api.BaseRoutes.InsightsForUser.Handle("/inactive_channels", api.APISessionRequired(minimumProfessionalLicense(rejectGuests(getTopInactiveChannelsForUserSince)))).Methods("GET") + api.BaseRoutes.InsightsForTeam.Handle("/inactive_channels", api.APISessionRequired(getTopInactiveChannelsForTeamSince)).Methods("GET") + api.BaseRoutes.InsightsForUser.Handle("/inactive_channels", api.APISessionRequired(getTopInactiveChannelsForUserSince)).Methods("GET") // New teammembers - api.BaseRoutes.InsightsForTeam.Handle("/team_members", api.APISessionRequired(minimumProfessionalLicense(rejectGuests(getNewTeamMembersSince)))).Methods("GET") + api.BaseRoutes.InsightsForTeam.Handle("/team_members", api.APISessionRequired(getNewTeamMembersSince)).Methods("GET") } // Top Reactions func getTopReactionsForTeamSince(c *Context, w http.ResponseWriter, r *http.Request) { + + // license and guest user check + permissionErr := minimumProfessionalLicense(c) + if permissionErr != nil { + c.Err = permissionErr + return + } + permissionErr = rejectGuests(c) + if permissionErr != nil { + c.Err = permissionErr + return + } + c.RequireTeamId() if c.Err != nil { return @@ -82,6 +95,18 @@ func getTopReactionsForTeamSince(c *Context, w http.ResponseWriter, r *http.Requ } func getTopReactionsForUserSince(c *Context, w http.ResponseWriter, r *http.Request) { + // license and guest user check + permissionErr := minimumProfessionalLicense(c) + if permissionErr != nil { + c.Err = permissionErr + return + } + permissionErr = rejectGuests(c) + if permissionErr != nil { + c.Err = permissionErr + return + } + c.Params.TeamId = r.URL.Query().Get("team_id") // TeamId is an optional parameter @@ -133,6 +158,18 @@ func getTopReactionsForUserSince(c *Context, w http.ResponseWriter, r *http.Requ // Top Channels func getTopChannelsForTeamSince(c *Context, w http.ResponseWriter, r *http.Request) { + // license and guest user check + permissionErr := minimumProfessionalLicense(c) + if permissionErr != nil { + c.Err = permissionErr + return + } + permissionErr = rejectGuests(c) + if permissionErr != nil { + c.Err = permissionErr + return + } + c.RequireTeamId() if c.Err != nil { return @@ -184,6 +221,18 @@ func getTopChannelsForTeamSince(c *Context, w http.ResponseWriter, r *http.Reque } func getTopChannelsForUserSince(c *Context, w http.ResponseWriter, r *http.Request) { + // license and guest user check + permissionErr := minimumProfessionalLicense(c) + if permissionErr != nil { + c.Err = permissionErr + return + } + permissionErr = rejectGuests(c) + if permissionErr != nil { + c.Err = permissionErr + return + } + c.Params.TeamId = r.URL.Query().Get("team_id") // TeamId is an optional parameter @@ -241,6 +290,18 @@ func getTopChannelsForUserSince(c *Context, w http.ResponseWriter, r *http.Reque // Top Threads func getTopThreadsForTeamSince(c *Context, w http.ResponseWriter, r *http.Request) { + // license and guest user check + permissionErr := minimumProfessionalLicense(c) + if permissionErr != nil { + c.Err = permissionErr + return + } + permissionErr = rejectGuests(c) + if permissionErr != nil { + c.Err = permissionErr + return + } + c.RequireTeamId() if c.Err != nil { return @@ -286,6 +347,18 @@ func getTopThreadsForTeamSince(c *Context, w http.ResponseWriter, r *http.Reques } func getTopThreadsForUserSince(c *Context, w http.ResponseWriter, r *http.Request) { + // license and guest user check + permissionErr := minimumProfessionalLicense(c) + if permissionErr != nil { + c.Err = permissionErr + return + } + permissionErr = rejectGuests(c) + if permissionErr != nil { + c.Err = permissionErr + return + } + c.Params.TeamId = r.URL.Query().Get("team_id") // restrict users with no access to team @@ -336,6 +409,18 @@ func getTopThreadsForUserSince(c *Context, w http.ResponseWriter, r *http.Reques // Top DMs func getTopDMsForUserSince(c *Context, w http.ResponseWriter, r *http.Request) { + // license and guest user check + permissionErr := minimumProfessionalLicense(c) + if permissionErr != nil { + c.Err = permissionErr + return + } + permissionErr = rejectGuests(c) + if permissionErr != nil { + c.Err = permissionErr + return + } + user, err := c.App.GetUser(c.AppContext.Session().UserId) if err != nil { c.Err = err @@ -367,6 +452,18 @@ func getTopDMsForUserSince(c *Context, w http.ResponseWriter, r *http.Request) { // Top Channels func getTopInactiveChannelsForTeamSince(c *Context, w http.ResponseWriter, r *http.Request) { + // license and guest user check + permissionErr := minimumProfessionalLicense(c) + if permissionErr != nil { + c.Err = permissionErr + return + } + permissionErr = rejectGuests(c) + if permissionErr != nil { + c.Err = permissionErr + return + } + c.RequireTeamId() if c.Err != nil { return @@ -411,6 +508,18 @@ func getTopInactiveChannelsForTeamSince(c *Context, w http.ResponseWriter, r *ht // top inactive channels func getTopInactiveChannelsForUserSince(c *Context, w http.ResponseWriter, r *http.Request) { + // license and guest user check + permissionErr := minimumProfessionalLicense(c) + if permissionErr != nil { + c.Err = permissionErr + return + } + permissionErr = rejectGuests(c) + if permissionErr != nil { + c.Err = permissionErr + return + } + c.Params.TeamId = r.URL.Query().Get("team_id") // TeamId is an optional parameter @@ -479,6 +588,18 @@ func postCountByDurationViewModel(c *Context, topChannelList *model.TopChannelLi } func getNewTeamMembersSince(c *Context, w http.ResponseWriter, r *http.Request) { + // license and guest user check + permissionErr := minimumProfessionalLicense(c) + if permissionErr != nil { + c.Err = permissionErr + return + } + permissionErr = rejectGuests(c) + if permissionErr != nil { + c.Err = permissionErr + return + } + c.RequireTeamId() if c.Err != nil { return From 31e6cdd0e6d334e35ca155e12a2d985b49af5f31 Mon Sep 17 00:00:00 2001 From: Harshil Sharma <18575143+harshilsharma63@users.noreply.github.com> Date: Thu, 20 Oct 2022 11:05:22 +0530 Subject: [PATCH 57/63] Slack attachment in email notifications (#20946) * First POC * Added author data and markdown support * WIP * styled fields * WIP * WIP * IMplementation done * Updated template * fixed mjml compilation error * removed card.mjml * Added test * Updated a styling * Updated a styling * CI * Updated generated templates * Explicitelly specified font for message attachments * Lint fix * Removed leftover existances of slack attachment --- app/notification_email.go | 89 +++++++ app/notification_email_test.go | 97 +++++++ templates/invite_body.mjml | 8 +- templates/messages_notification.html | 244 +++++++++++++++++- templates/messages_notification.mjml | 10 +- templates/partials/card.mjml | 42 --- templates/partials/message_attachment.css | 146 +++++++++++ templates/partials/message_attachment.html | 95 +++++++ .../partials/message_attachment_styles.mjml | 5 + templates/partials/message_avatar_col.mjml | 3 + templates/partials/message_button.mjml | 5 + templates/partials/sender_info_col.mjml | 30 +++ 12 files changed, 729 insertions(+), 45 deletions(-) delete mode 100644 templates/partials/card.mjml create mode 100644 templates/partials/message_attachment.css create mode 100644 templates/partials/message_attachment.html create mode 100644 templates/partials/message_attachment_styles.mjml create mode 100644 templates/partials/message_avatar_col.mjml create mode 100644 templates/partials/message_button.mjml create mode 100644 templates/partials/sender_info_col.mjml diff --git a/app/notification_email.go b/app/notification_email.go index 914b729a01..965dd9a292 100644 --- a/app/notification_email.go +++ b/app/notification_email.go @@ -201,6 +201,18 @@ func truncateUserNames(name string, i int) string { return name } +type FieldRow struct { + Cells []*model.SlackAttachmentField +} + +type EmailMessageAttachment struct { + model.SlackAttachment + + Pretext template.HTML + Text template.HTML + FieldRows []FieldRow +} + type postData struct { SenderName string ChannelName string @@ -211,6 +223,7 @@ type postData struct { Time string ShowChannelIcon bool OtherChannelMembersCount int + MessageAttachments []*EmailMessageAttachment } /** @@ -245,6 +258,7 @@ func (a *App) getNotificationEmailBody(c request.CTX, recipient *model.User, pos } pData.Message = template.HTML(normalizedPostMessage) pData.Time = translateFunc("app.notification.body.dm.time", messageTime) + pData.MessageAttachments = a.processMessageAttachments(post) } data := a.Srv().EmailService.NewEmailTemplateData(recipient.Locale) @@ -306,6 +320,81 @@ func (a *App) getNotificationEmailBody(c request.CTX, recipient *model.User, pos return a.Srv().TemplatesContainer().RenderToString("messages_notification", data) } +func (a *App) processMessageAttachments(post *model.Post) []*EmailMessageAttachment { + emailMessageAttachments := []*EmailMessageAttachment{} + + for _, messageAttachment := range post.Attachments() { + emailMessageAttachment := &EmailMessageAttachment{ + SlackAttachment: *messageAttachment, + Pretext: a.prepareTextForEmail(messageAttachment.Pretext), + Text: a.prepareTextForEmail(messageAttachment.Text), + } + + stripedTitle, err := utils.StripMarkdown(emailMessageAttachment.Title) + if err != nil { + mlog.Warn("Failed parse to markdown from messageatatchment title", mlog.String("post_id", post.Id), mlog.Err(err)) + stripedTitle = "" + } + + emailMessageAttachment.Title = stripedTitle + + shortFieldRow := FieldRow{} + + for i := range messageAttachment.Fields { + // Create a new instance to avoid altering the original pointer reference + // We update field value to parse markdown. + // If we do that on the original pointer, the rendered text in mattermost + // becomes invalid as its no longer a markdown string, but rather an HTML string. + field := &model.SlackAttachmentField{ + Title: messageAttachment.Fields[i].Title, + Value: messageAttachment.Fields[i].Value, + Short: messageAttachment.Fields[i].Short, + } + + if stringValue, ok := field.Value.(string); ok { + field.Value = a.prepareTextForEmail(stringValue) + } + + if !field.Short { + if len(shortFieldRow.Cells) > 0 { + emailMessageAttachment.FieldRows = append(emailMessageAttachment.FieldRows, shortFieldRow) + shortFieldRow = FieldRow{} + } + + emailMessageAttachment.FieldRows = append(emailMessageAttachment.FieldRows, FieldRow{[]*model.SlackAttachmentField{field}}) + } else { + shortFieldRow.Cells = append(shortFieldRow.Cells, field) + + if len(shortFieldRow.Cells) == 2 { + emailMessageAttachment.FieldRows = append(emailMessageAttachment.FieldRows, shortFieldRow) + shortFieldRow = FieldRow{} + } + } + } + + // collect any leftover short fields + if len(shortFieldRow.Cells) > 0 { + emailMessageAttachment.FieldRows = append(emailMessageAttachment.FieldRows, shortFieldRow) + shortFieldRow = FieldRow{} + } + + emailMessageAttachments = append(emailMessageAttachments, emailMessageAttachment) + } + + return emailMessageAttachments +} + +func (a *App) prepareTextForEmail(text string) template.HTML { + escapedText := html.EscapeString(text) + markdownText, err := utils.MarkdownToHTML(escapedText) + if err != nil { + mlog.Warn("Encountered error while converting markdown to HTML", mlog.Err(err)) + return template.HTML(text) + } + + return template.HTML(markdownText) +} + type formattedPostTime struct { Time time.Time Year string diff --git a/app/notification_email_test.go b/app/notification_email_test.go index 8b48b50035..4d0f77f433 100644 --- a/app/notification_email_test.go +++ b/app/notification_email_test.go @@ -339,6 +339,103 @@ func TestGetNotificationEmailBodyFullNotificationLocaleTime24Hour(t *testing.T) require.Contains(t, body, "14:30", fmt.Sprintf("Expected email text '14:30'. Got %s", body)) } +func TestGetNotificationEmailBodyFullNotificationWithSlackAttachments(t *testing.T) { + th := SetupWithStoreMock(t) + defer th.TearDown() + + recipient := &model.User{} + post := &model.Post{ + Message: "This is the message", + } + + messageAttachments := []*model.SlackAttachment{ + { + Color: "#FF0000", + Pretext: "message attachment 1 pretext", + AuthorName: "author name", + AuthorLink: "https://example.com/slack_attachment_1/author_link", + AuthorIcon: "https://example.com/slack_attachment_1/author_icon", + Title: "message attachment 1 title", + TitleLink: "https://example.com/slack_attachment_1/title_link", + Text: "message attachment 1 text", + ImageURL: "https://example.com/slack_attachment_1/image", + ThumbURL: "https://example.com/slack_attachment_1/thumb", + Fields: []*model.SlackAttachmentField{ + { + Short: true, + Title: "message attachment 1 field 1 title", + Value: "message attachment 1 field 1 value", + }, + { + Short: false, + Title: "message attachment 1 field 2 title", + Value: "message attachment 1 field 2 value", + }, + { + Short: true, + Title: "message attachment 1 field 3 title", + Value: "message attachment 1 field 3 value", + }, + { + Short: true, + Title: "message attachment 1 field 4 title", + Value: "message attachment 1 field 4 value", + }, + }, + }, + { + Color: "#FF0000", + Pretext: "message attachment 2 pretext", + AuthorName: "author name 2", + Text: "message attachment 2 text", + }, + } + + model.ParseSlackAttachment(post, messageAttachments) + + channel := &model.Channel{ + DisplayName: "ChannelName", + Type: model.ChannelTypeOpen, + } + + channelName := "ChannelName" + senderName := "sender" + teamName := "testteam" + teamURL := "http://localhost:8065/testteam" + emailNotificationContentsType := model.EmailNotificationContentsFull + translateFunc := i18n.GetUserTranslations("en") + + storeMock := th.App.Srv().Store().(*mocks.Store) + teamStoreMock := mocks.TeamStore{} + teamStoreMock.On("GetByName", "testteam").Return(&model.Team{Name: "testteam"}, nil) + storeMock.On("Team").Return(&teamStoreMock) + + body, err := th.App.getNotificationEmailBody(th.Context, recipient, post, channel, channelName, senderName, teamName, teamURL, emailNotificationContentsType, true, translateFunc, "user-avatar.png") + require.NoError(t, err) + require.Contains(t, body, "#FF0000") + require.Contains(t, body, "message attachment 1 pretext") + require.Contains(t, body, "author name") + require.Contains(t, body, "https://example.com/slack_attachment_1/author_link") + require.Contains(t, body, "https://example.com/slack_attachment_1/author_icon") + require.Contains(t, body, "message attachment 1 title") + require.Contains(t, body, "https://example.com/slack_attachment_1/title_link") + require.Contains(t, body, "message attachment 1 text") + require.Contains(t, body, "https://example.com/slack_attachment_1/image") + require.Contains(t, body, "https://example.com/slack_attachment_1/thumb") + require.Contains(t, body, "message attachment 1 field 1 title") + require.Contains(t, body, "message attachment 1 field 1 value") + require.Contains(t, body, "message attachment 1 field 2 title") + require.Contains(t, body, "message attachment 1 field 2 value") + require.Contains(t, body, "message attachment 1 field 3 title") + require.Contains(t, body, "message attachment 1 field 3 value") + require.Contains(t, body, "message attachment 1 field 4 title") + require.Contains(t, body, "message attachment 1 field 4 value") + require.Contains(t, body, "https://example.com/slack_attachment_1/thumb") + require.Contains(t, body, "message attachment 2 pretext") + require.Contains(t, body, "author name 2") + require.Contains(t, body, "message attachment 2 text") +} + // from here func TestGetNotificationEmailBodyGenericNotificationPublicChannel(t *testing.T) { th := SetupWithStoreMock(t) diff --git a/templates/invite_body.mjml b/templates/invite_body.mjml index 869a359b2c..d16c20af68 100644 --- a/templates/invite_body.mjml +++ b/templates/invite_body.mjml @@ -8,7 +8,13 @@ {{if .Props.Message}} {{range .Props.Posts}}
- + + + + + + +
{{end}}
{{else}} diff --git a/templates/messages_notification.html b/templates/messages_notification.html index 9c40227203..7b47478cfa 100644 --- a/templates/messages_notification.html +++ b/templates/messages_notification.html @@ -356,7 +356,160 @@ max-width: 100% !important; } } + + .messageAttachments * { + font-family: Open Sans, sans-serif !important; + } + + .messageAttachmentContent { + padding: 0px; + border: 1px solid rgba(63, 67, 80, 0.16); + margin-bottom: 20px; + border-radius: 0 4px 4px 0; + } + + .messageAttachmentContent>table, + .attachment__body { + font-family: Open Sans, sans-serif; + text-align: left; + font-size: 14px; + line-height: 20px; + color: #3F4350; + } + + .messageAttachmentContent .attachment__author-icon { + width: 14px; + height: 14px; + margin-right: 5px; + border-radius: 50px; + vertical-align: middle; + } + + .attachment__author-name { + opacity: 0.6; + } + + .messageAttachmentContent p { + margin: 0; + } + + .attachment__title { + padding: 0; + margin: 5px 0; + font-size: 14px; + font-weight: 600; + line-height: 18px; + } + + .attachment__image { + max-height: 300px; + border: 1px solid transparent; + margin-bottom: 1em; + } + + .attachment__thumb-image { + max-width: 100%; + max-height: 75px; + } + + .attachment__thumb-container { + max-width: 80px; + width: max-content; + } + + .attachment__wrapper { + width: 100%; + display: flex; + flex-direction: row; + gap: 12px; + } + + .attachment__body { + flex: 1; + } + + .messageAttachmentContent>table.attachment__footer-container { + color: #a3a3a3; + font-size: 12px; + } + + .attachment__footer-icon { + width: 16px; + height: 16px; + } + + .attachment__footer-container { + color: #a3a3a3; + font-size: 12px; + } + + .messageAttachment_title { + padding-top: 1em; + font-weight: 600; + margin-bottom: 4px; + } + + .pretext h1 { + font-size: 28px; + line-height: 32px; + } + + .pretext h2 { + font-size: 25px; + line-height: 30px; + } + + .pretext h3 { + font-size: 22px; + line-height: 25px; + } + + .pretext h4 { + font-size: 19px; + line-height: 24px; + } + + .pretext h5 { + font-size: 15px; + line-height: 20px; + } + + .pretext h6 { + font-size: 1em; + line-height: 1.4em; + } + + .pretext>* { + margin-bottom: 16px; + } + + .messageAttachments { + margin-top: 22px; + } + + .messageAttachments h1, + h2, + h3, + h4, + h5, + h6 { + font-weight: 500; + } + + code { + padding: 2px 4px; + font-size: 90%; + background-color: rgba(63, 67, 80, 0.1); + border-radius: 4px; + } + + .messageAttachments a { + background-color: unset !important; + border: none !important; + padding: unset !important; + } + @@ -507,7 +660,96 @@
- + {{.Props.VerifyButton}}
-

{{.Props.QuestionTitle}}

-

{{.Props.EmailInfo1}}{{.Props.SupportEmail}}

+

{{.Props.QuestionTitle}}

+

{{.Props.EmailInfo1}}{{.Props.SupportEmail}}

-

{{.Props.QuestionTitle}}

-

{{.Props.EmailInfo1}}{{.Props.SupportEmail}}

+

{{.Props.QuestionTitle}}

+

{{.Props.EmailInfo1}}{{.Props.SupportEmail}}

-

{{.Props.Title}}

+

{{.Props.Title}}

-

{{.Props.Title}}

+

{{.Props.Title}}

-

{{.Props.Title}}

+

{{.Props.Title}}

- + + {{if .MessageAttachments}} +
+ {{range .MessageAttachments}} +
+
+ {{.Pretext}} +
+
+ + + {{if or .AuthorIcon .AuthorName}} + + + + {{end}} + {{if .Title}} + + + + {{end}} + +
+ {{if .AuthorLink}}{{end}} + {{if .AuthorIcon}}attachment author icon{{end}} + {{if .AuthorName}}{{.AuthorName}}{{end}} + {{if .AuthorLink}}{{end}} +
+

+ {{if .TitleLink}}{{end}} + {{.Title}} + {{if .Title}}{{end}} +

+
+
+
+ + + + + + {{if .ImageURL}} + + + + {{end}} + +
+
{{.Text}}
+
+ +
+ + {{range .FieldRows}} + + {{ $length := len .Cells }} + {{range .Cells}} + + {{end}} + + {{end}} +
+
{{.Title}}
+
{{.Value}}
+
+
+ {{if .ThumbURL}} +
+ +
+ {{end}} +
+ {{if .Footer}} + + + + + + + + {{end}} +
+
+ {{end}} +
+ {{end}} +
diff --git a/templates/messages_notification.mjml b/templates/messages_notification.mjml index ad840ff420..a2b53b6b16 100644 --- a/templates/messages_notification.mjml +++ b/templates/messages_notification.mjml @@ -1,13 +1,21 @@ + {{range .Props.Posts}}
- + + + + + + + +
{{end}}
diff --git a/templates/partials/card.mjml b/templates/partials/card.mjml deleted file mode 100644 index f1c835e04c..0000000000 --- a/templates/partials/card.mjml +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - -
- - - - - {{.Message}} - - - - {{if .MessageURL}} - {{$.Props.MessageButton}} - {{end}} - - - diff --git a/templates/partials/message_attachment.css b/templates/partials/message_attachment.css new file mode 100644 index 0000000000..ee65c7f8a3 --- /dev/null +++ b/templates/partials/message_attachment.css @@ -0,0 +1,146 @@ +.messageAttachments * { + font-family: Open Sans, sans-serif !important; +} + +.messageAttachmentContent { + padding: 0px; + border: 1px solid rgba(63, 67, 80, 0.16); + margin-bottom: 20px; + border-radius: 0 4px 4px 0; +} + +.messageAttachmentContent > table, .attachment__body { + font-family: Open Sans, sans-serif; + text-align: left; + font-size: 14px; + line-height: 20px; + color: #3F4350; +} + +.messageAttachmentContent .attachment__author-icon { + width: 14px; + height: 14px; + margin-right: 5px; + border-radius: 50px; + vertical-align: middle; +} + +.attachment__author-name { + opacity: 0.6; +} + +.messageAttachmentContent p { + margin: 0; +} + +.attachment__title { + padding: 0; + margin: 5px 0; + font-size: 14px; + font-weight: 600; + line-height: 18px; +} + +.attachment__image { + max-height: 300px; + border: 1px solid transparent; + margin-bottom: 1em; +} + + +.attachment__thumb-image { + max-width: 100%; + max-height: 75px; +} + +.attachment__thumb-container { + max-width: 80px; + width: max-content; +} + +.attachment__wrapper { + width: 100%; + display: flex; + flex-direction: row; + gap: 12px; +} + +.attachment__body { + flex: 1; +} + +.messageAttachmentContent > table.attachment__footer-container { + color: #a3a3a3; + font-size: 12px; +} + +.attachment__footer-icon { + width: 16px; + height: 16px; +} + +.attachment__footer-container { + color: #a3a3a3; + font-size: 12px; +} + +.messageAttachment_title { + padding-top: 1em; + font-weight: 600; + margin-bottom: 4px; +} + +.pretext h1 { + font-size: 28px; + line-height: 32px; +} + +.pretext h2 { + font-size: 25px; + line-height: 30px; +} + +.pretext h3 { + font-size: 22px; + line-height: 25px; +} + +.pretext h4 { + font-size: 19px; + line-height: 24px; +} + +.pretext h5 { + font-size: 15px; + line-height: 20px; +} + +.pretext h6 { + font-size: 1em; + line-height: 1.4em; +} + +.pretext > * { + margin-bottom: 16px; +} + +.messageAttachments { + margin-top: 22px; +} + +.messageAttachments h1, h2, h3, h4, h5, h6 { + font-weight: 500; +} + +code { + padding: 2px 4px; + font-size: 90%; + background-color: rgba(63, 67, 80, 0.1); + border-radius: 4px; +} + +.messageAttachments a { + background-color: unset !important; + border: none !important; + padding: unset !important; +} diff --git a/templates/partials/message_attachment.html b/templates/partials/message_attachment.html new file mode 100644 index 0000000000..68dd5fe21b --- /dev/null +++ b/templates/partials/message_attachment.html @@ -0,0 +1,95 @@ +{{if .MessageAttachments}} +
+ {{range .MessageAttachments}} +
+
+ {{.Pretext}} +
+
+
-
-
{{.SenderName}}
- {{if .Time}} -
{{.Time}}
- {{end}} - {{if .ChannelName}} -
- {{if .ShowChannelIcon}} - - {{end}} -
- {{if .OtherChannelMembersCount}} - {{.OtherChannelMembersCount}} - {{end}} - {{.ChannelName}} -
-
- {{end}} -
-
+ + {{if or .AuthorIcon .AuthorName}} + + + + {{end}} + {{if .Title}} + + + + {{end}} + +
+ {{if .AuthorLink}}{{end}} + {{if .AuthorIcon}}attachment author icon{{end}} + {{if .AuthorName}}{{.AuthorName}}{{end}} + {{if .AuthorLink}}{{end}} +
+

+ {{if .TitleLink}}{{end}} + {{.Title}} + {{if .Title}}{{end}} +

+
+ +
+
+ + + + + + {{if .ImageURL}} + + + + {{end}} +
+
{{.Text}}
+
+ +
+ + + {{range .FieldRows}} + + {{ $length := len .Cells }} + {{range .Cells}} + + {{end}} + + {{end}} +
+
{{.Title}}
+
{{.Value}}
+
+
+ + {{if .ThumbURL}} +
+ +
+ {{end}} +
+ + {{if .Footer}} + + + + + + + + {{end}} + +
+ + {{end}} + +{{end}} diff --git a/templates/partials/message_attachment_styles.mjml b/templates/partials/message_attachment_styles.mjml new file mode 100644 index 0000000000..0e0de4dd3a --- /dev/null +++ b/templates/partials/message_attachment_styles.mjml @@ -0,0 +1,5 @@ + + + + + diff --git a/templates/partials/message_avatar_col.mjml b/templates/partials/message_avatar_col.mjml new file mode 100644 index 0000000000..dc4686896d --- /dev/null +++ b/templates/partials/message_avatar_col.mjml @@ -0,0 +1,3 @@ + + + diff --git a/templates/partials/message_button.mjml b/templates/partials/message_button.mjml new file mode 100644 index 0000000000..997edac1bc --- /dev/null +++ b/templates/partials/message_button.mjml @@ -0,0 +1,5 @@ + + {{if .MessageURL}} + {{$.Props.MessageButton}} + {{end}} + diff --git a/templates/partials/sender_info_col.mjml b/templates/partials/sender_info_col.mjml new file mode 100644 index 0000000000..69b37d1853 --- /dev/null +++ b/templates/partials/sender_info_col.mjml @@ -0,0 +1,30 @@ + + + + +
+
{{.SenderName}}
+ {{if .Time}} +
{{.Time}}
+ {{end}} + {{if .ChannelName}} +
+ {{if .ShowChannelIcon}} + + {{end}} +
+ {{if .OtherChannelMembersCount}} + {{.OtherChannelMembersCount}} + {{end}} + {{.ChannelName}} +
+
+ {{end}} +
+ + +
+ + {{.Message}} + +
From c39c05e93ccd9397b72fd07186233ef8b13fedbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Andr=C3=A9s=20V=C3=A9lez=20Vidal?= Date: Thu, 20 Oct 2022 16:01:15 +0200 Subject: [PATCH 58/63] Replace 500 errors with more meaningful error codes (#21439) * Replace 500 errors with more meaningful error codes * replace forbidden with badrequest for cloud customer fetch, rename parameter name --- api4/cloud.go | 16 ++++++------ api4/cloud_test.go | 61 ++++++++++++++++++++++++++++++++++++++++++++++ model/client4.go | 4 +-- 3 files changed, 71 insertions(+), 10 deletions(-) diff --git a/api4/cloud.go b/api4/cloud.go index e86a9b0aca..a88ac919b3 100644 --- a/api4/cloud.go +++ b/api4/cloud.go @@ -158,14 +158,14 @@ func requestCloudTrial(c *Context, w http.ResponseWriter, r *http.Request) { // check if the email needs to be set bodyBytes, err := io.ReadAll(r.Body) if err != nil { - c.Err = model.NewAppError("Api4.requestCloudTrial", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err) + c.Err = model.NewAppError("Api4.requestCloudTrial", "api.cloud.app_error", nil, "", http.StatusBadRequest).Wrap(err) return } // this value will not be empty when both emails (user admin and CWS customer) are not business email and - // we need to request a new email from the user via the request business email modal + // a new business email was provided via the request business email modal var startTrialRequest *model.StartCloudTrialRequest if err = json.Unmarshal(bodyBytes, &startTrialRequest); err != nil { - c.Err = model.NewAppError("Api4.requestCloudTrial", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err) + c.Err = model.NewAppError("Api4.requestCloudTrial", "api.cloud.app_error", nil, "", http.StatusBadRequest).Wrap(err) return } @@ -199,20 +199,20 @@ func validateBusinessEmail(c *Context, w http.ResponseWriter, r *http.Request) { user, appErr := c.App.GetUser(c.AppContext.Session().UserId) if appErr != nil { - c.Err = model.NewAppError("Api4.validateBusinessEmail", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(appErr) + c.Err = model.NewAppError("Api4.validateBusinessEmail", "api.cloud.request_error", nil, "", http.StatusForbidden).Wrap(appErr) return } bodyBytes, err := io.ReadAll(r.Body) if err != nil { - c.Err = model.NewAppError("Api4.requestCloudTrial", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err) + c.Err = model.NewAppError("Api4.requestCloudTrial", "api.cloud.app_error", nil, "", http.StatusBadRequest).Wrap(err) return } var emailToValidate *model.ValidateBusinessEmailRequest err = json.Unmarshal(bodyBytes, &emailToValidate) if err != nil { - c.Err = model.NewAppError("Api4.requestCloudTrial", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err) + c.Err = model.NewAppError("Api4.requestCloudTrial", "api.cloud.app_error", nil, "", http.StatusBadRequest).Wrap(err) return } @@ -244,14 +244,14 @@ func validateWorkspaceBusinessEmail(c *Context, w http.ResponseWriter, r *http.R user, userErr := c.App.GetUser(c.AppContext.Session().UserId) if userErr != nil { - c.Err = model.NewAppError("Api4.validateWorkspaceBusinessEmail", "api.cloud.request_error", nil, userErr.Error(), http.StatusInternalServerError) + c.Err = userErr return } // get the cloud customer email to validate if is a valid business email cloudCustomer, err := c.App.Cloud().GetCloudCustomer(user.Id) if err != nil { - c.Err = model.NewAppError("Api4.validateWorkspaceBusinessEmail", "api.cloud.request_error", nil, err.Error(), http.StatusInternalServerError) + c.Err = model.NewAppError("Api4.validateWorkspaceBusinessEmail", "api.cloud.request_error", nil, err.Error(), http.StatusBadRequest) return } emailErr := c.App.Cloud().ValidateBusinessEmail(user.Id, cloudCustomer.Email) diff --git a/api4/cloud_test.go b/api4/cloud_test.go index 999f1077e4..2092342c89 100644 --- a/api4/cloud_test.go +++ b/api4/cloud_test.go @@ -296,6 +296,20 @@ func Test_requestTrial(t *testing.T) { require.Equal(t, subscriptionChanged, subscription) require.Equal(t, http.StatusOK, r.StatusCode, "Status OK") }) + + t.Run("Empty body returns bad request", func(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + th.Client.Login(th.BasicUser.Email, th.BasicUser.Password) + + th.App.Srv().SetLicense(model.NewTestLicense("cloud")) + + r, err := th.SystemAdminClient.DoAPIPutBytes("/cloud/request-trial", nil) + require.Error(t, err) + closeBody(r) + require.Equal(t, http.StatusBadRequest, r.StatusCode, "Status Bad Request") + }) } func Test_validateBusinessEmail(t *testing.T) { @@ -373,6 +387,20 @@ func Test_validateBusinessEmail(t *testing.T) { require.NoError(t, err) require.Equal(t, http.StatusOK, res.StatusCode, "200") }) + + t.Run("Empty body returns bad request", func(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + th.Client.Login(th.BasicUser.Email, th.BasicUser.Password) + + th.App.Srv().SetLicense(model.NewTestLicense("cloud")) + + r, err := th.SystemAdminClient.DoAPIPostBytes("/cloud/validate-business-email", nil) + require.Error(t, err) + closeBody(r) + require.Equal(t, http.StatusBadRequest, r.StatusCode, "Status Bad Request") + }) } func Test_validateWorkspaceBusinessEmail(t *testing.T) { @@ -442,6 +470,39 @@ func Test_validateWorkspaceBusinessEmail(t *testing.T) { _, err := th.SystemAdminClient.ValidateWorkspaceBusinessEmail() require.NoError(t, err) }) + + t.Run("Error while grabbing the cloud customer returns bad request", func(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + th.Client.Login(th.BasicUser.Email, th.BasicUser.Password) + + th.App.Srv().SetLicense(model.NewTestLicense("cloud")) + + cloud := mocks.CloudInterface{} + + cloudCustomerInfo := model.CloudCustomerInfo{ + Email: "badrequest@gmail.com", + } + + // return an error while getting the cloud customer so we validate the forbidden error return + cloud.Mock.On("GetCloudCustomer", th.SystemAdminUser.Id).Return(nil, errors.New("error while gettings the cloud customer")) + + // required cloud mocks so the request doesn't fail + cloud.Mock.On("ValidateBusinessEmail", th.SystemAdminUser.Id, cloudCustomerInfo.Email).Return(errors.New("invalid email")) + cloud.Mock.On("ValidateBusinessEmail", th.SystemAdminUser.Id, th.SystemAdminUser.Email).Return(nil) + + cloudImpl := th.App.Srv().Cloud + defer func() { + th.App.Srv().Cloud = cloudImpl + }() + th.App.Srv().Cloud = &cloud + + r, err := th.SystemAdminClient.DoAPIPostBytes("/cloud/validate-workspace-business-email", nil) + require.Error(t, err) + closeBody(r) + require.Equal(t, http.StatusBadRequest, r.StatusCode, "Status Bad Request") + }) } func TestGetCloudProducts(t *testing.T) { diff --git a/model/client4.go b/model/client4.go index cfe0f9244e..b569a8fa86 100644 --- a/model/client4.go +++ b/model/client4.go @@ -8004,8 +8004,8 @@ func (c *Client4) ConfirmCustomerPayment(confirmRequest *ConfirmPaymentMethodReq return BuildResponse(r), nil } -func (c *Client4) RequestCloudTrial(email *StartCloudTrialRequest) (*Subscription, *Response, error) { - payload, err := json.Marshal(email) +func (c *Client4) RequestCloudTrial(cloudTrialRequest *StartCloudTrialRequest) (*Subscription, *Response, error) { + payload, err := json.Marshal(cloudTrialRequest) if err != nil { return nil, nil, NewAppError("RequestCloudTrial", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err) } From 093c657e56902dc066700898a251964bd16e5ddc Mon Sep 17 00:00:00 2001 From: cyrilzhang-mm <112951043+cyrilzhang-mm@users.noreply.github.com> Date: Thu, 20 Oct 2022 14:37:24 -0400 Subject: [PATCH 59/63] [MM-45052] Add error checking in insights API (#21048) --- api4/insights.go | 60 +++++++++++++++++++++++++++++++++++-------- api4/insights_test.go | 24 +++++++++++++++++ app/channel_test.go | 8 +++--- app/reaction_test.go | 4 +-- i18n/en.json | 4 +++ model/insights.go | 18 +++++++++++++ 6 files changed, 102 insertions(+), 16 deletions(-) diff --git a/api4/insights.go b/api4/insights.go index fce03cfa04..9d74a57c2f 100644 --- a/api4/insights.go +++ b/api4/insights.go @@ -73,7 +73,11 @@ func getTopReactionsForTeamSince(c *Context, w http.ResponseWriter, r *http.Requ return } - startTime := model.StartOfDayForTimeRange(c.Params.TimeRange, user.GetTimezoneLocation()) + startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, user.GetTimezoneLocation()) + if appErr != nil { + c.Err = appErr + return + } topReactionList, appErr := c.App.GetTopReactionsForTeamSince(c.Params.TeamId, c.AppContext.Session().UserId, &model.InsightsOpts{ StartUnixMilli: startTime.UnixMilli(), @@ -134,7 +138,11 @@ func getTopReactionsForUserSince(c *Context, w http.ResponseWriter, r *http.Requ return } - startTime := model.StartOfDayForTimeRange(c.Params.TimeRange, user.GetTimezoneLocation()) + startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, user.GetTimezoneLocation()) + if appErr != nil { + c.Err = appErr + return + } topReactionList, appErr := c.App.GetTopReactionsForUserSince(c.AppContext.Session().UserId, c.Params.TeamId, &model.InsightsOpts{ StartUnixMilli: startTime.UnixMilli(), @@ -193,7 +201,11 @@ func getTopChannelsForTeamSince(c *Context, w http.ResponseWriter, r *http.Reque } loc := user.GetTimezoneLocation() - startTime := model.StartOfDayForTimeRange(c.Params.TimeRange, loc) + startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, loc) + if appErr != nil { + c.Err = appErr + return + } topChannels, appErr := c.App.GetTopChannelsForTeamSince(c.AppContext, c.Params.TeamId, c.AppContext.Session().UserId, &model.InsightsOpts{ StartUnixMilli: startTime.UnixMilli(), @@ -261,7 +273,11 @@ func getTopChannelsForUserSince(c *Context, w http.ResponseWriter, r *http.Reque } loc := user.GetTimezoneLocation() - startTime := model.StartOfDayForTimeRange(c.Params.TimeRange, loc) + startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, loc) + if appErr != nil { + c.Err = appErr + return + } topChannels, appErr := c.App.GetTopChannelsForUserSince(c.AppContext, c.AppContext.Session().UserId, c.Params.TeamId, &model.InsightsOpts{ StartUnixMilli: startTime.UnixMilli(), @@ -325,7 +341,11 @@ func getTopThreadsForTeamSince(c *Context, w http.ResponseWriter, r *http.Reques return } - startTime := model.StartOfDayForTimeRange(c.Params.TimeRange, user.GetTimezoneLocation()) + startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, user.GetTimezoneLocation()) + if appErr != nil { + c.Err = appErr + return + } topThreads, appErr := c.App.GetTopThreadsForTeamSince(c.AppContext, c.Params.TeamId, c.AppContext.Session().UserId, &model.InsightsOpts{ StartUnixMilli: startTime.UnixMilli(), @@ -386,7 +406,11 @@ func getTopThreadsForUserSince(c *Context, w http.ResponseWriter, r *http.Reques } } - startTime := model.StartOfDayForTimeRange(c.Params.TimeRange, user.GetTimezoneLocation()) + startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, user.GetTimezoneLocation()) + if appErr != nil { + c.Err = appErr + return + } topThreads, appErr := c.App.GetTopThreadsForUserSince(c.AppContext, c.Params.TeamId, c.AppContext.Session().UserId, &model.InsightsOpts{ StartUnixMilli: startTime.UnixMilli(), @@ -427,7 +451,11 @@ func getTopDMsForUserSince(c *Context, w http.ResponseWriter, r *http.Request) { return } - startTime := model.StartOfDayForTimeRange(c.Params.TimeRange, user.GetTimezoneLocation()) + startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, user.GetTimezoneLocation()) + if appErr != nil { + c.Err = appErr + return + } topDMs, err := c.App.GetTopDMsForUserSince(user.Id, &model.InsightsOpts{ StartUnixMilli: startTime.UnixMilli(), @@ -487,7 +515,11 @@ func getTopInactiveChannelsForTeamSince(c *Context, w http.ResponseWriter, r *ht } loc := user.GetTimezoneLocation() - startTime := model.StartOfDayForTimeRange(c.Params.TimeRange, loc) + startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, loc) + if appErr != nil { + c.Err = appErr + return + } topChannels, err := c.App.GetTopInactiveChannelsForTeamSince(c.AppContext, c.Params.TeamId, c.AppContext.Session().UserId, &model.InsightsOpts{ StartUnixMilli: startTime.UnixMilli(), @@ -548,7 +580,11 @@ func getTopInactiveChannelsForUserSince(c *Context, w http.ResponseWriter, r *ht } loc := user.GetTimezoneLocation() - startTime := model.StartOfDayForTimeRange(c.Params.TimeRange, loc) + startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, loc) + if appErr != nil { + c.Err = appErr + return + } topChannels, err := c.App.GetTopInactiveChannelsForUserSince(c.AppContext, c.Params.TeamId, c.AppContext.Session().UserId, &model.InsightsOpts{ StartUnixMilli: startTime.UnixMilli(), @@ -622,7 +658,11 @@ func getNewTeamMembersSince(c *Context, w http.ResponseWriter, r *http.Request) return } loc := user.GetTimezoneLocation() - startTime := model.StartOfDayForTimeRange(c.Params.TimeRange, loc) + startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, loc) + if appErr != nil { + c.Err = appErr + return + } ntms, count, err := c.App.GetNewTeamMembersSince(c.AppContext, c.Params.TeamId, &model.InsightsOpts{ StartUnixMilli: startTime.UnixMilli(), diff --git a/api4/insights_test.go b/api4/insights_test.go index 089bd26842..2443c1ea26 100644 --- a/api4/insights_test.go +++ b/api4/insights_test.go @@ -230,6 +230,12 @@ func TestGetTopReactionsForTeamSince(t *testing.T) { CheckNotFoundStatus(t, resp) }) + t.Run("get-top-reactions-for-team-since invalid time range", func(t *testing.T) { + _, resp, err := client.GetTopReactionsForTeamSince(teamId, "7_days", 0, 5) + require.Error(t, err) + CheckBadRequestStatus(t, resp) + }) + t.Run("get-top-reactions-for-team-since not a member of team", func(t *testing.T) { th.UnlinkUserFromTeam(th.BasicUser, th.BasicTeam) _, resp, err := client.GetTopReactionsForTeamSince(teamId, model.TimeRangeToday, 0, 5) @@ -417,6 +423,12 @@ func TestGetTopReactionsForUserSince(t *testing.T) { CheckNotFoundStatus(t, resp) }) + t.Run("get-top-reactions-for-user-since invalid time range", func(t *testing.T) { + _, resp, err := client.GetTopReactionsForUserSince(teamId, "7_days", 0, 5) + require.Error(t, err) + CheckBadRequestStatus(t, resp) + }) + t.Run("get-top-reactions-for-user-since not a member of team", func(t *testing.T) { th.UnlinkUserFromTeam(th.BasicUser, th.BasicTeam) _, resp, err := client.GetTopReactionsForUserSince(teamId, model.TimeRangeToday, 0, 5) @@ -515,6 +527,12 @@ func TestGetTopChannelsForTeamSince(t *testing.T) { CheckNotFoundStatus(t, resp) }) + t.Run("get-top-channels-for-team-since invalid time range", func(t *testing.T) { + _, resp, err := client.GetTopChannelsForTeamSince(teamId, "7_days", 0, 5) + assert.Error(t, err) + CheckBadRequestStatus(t, resp) + }) + t.Run("get-top-channels-for-team-since not a member of team", func(t *testing.T) { th.UnlinkUserFromTeam(th.BasicUser, th.BasicTeam) _, resp, err := client.GetTopChannelsForTeamSince(teamId, model.TimeRangeToday, 0, 5) @@ -592,6 +610,12 @@ func TestGetTopChannelsForUserSince(t *testing.T) { CheckNotFoundStatus(t, resp) }) + t.Run("get-top-channels-for-user-since invalid time range", func(t *testing.T) { + _, resp, err := client.GetTopChannelsForUserSince(teamId, "7_days", 0, 5) + assert.Error(t, err) + CheckBadRequestStatus(t, resp) + }) + t.Run("get-top-channels-for-user-since not a member of team", func(t *testing.T) { th.UnlinkUserFromTeam(th.BasicUser, th.BasicTeam) _, resp, err := client.GetTopChannelsForUserSince(teamId, model.TimeRangeToday, 0, 5) diff --git a/app/channel_test.go b/app/channel_test.go index 260b46d641..0361122027 100644 --- a/app/channel_test.go +++ b/app/channel_test.go @@ -2498,7 +2498,7 @@ func TestGetTopChannelsForTeamSince(t *testing.T) { {ID: channel5.Id, MessageCount: 2}, } - timeRange := model.StartOfDayForTimeRange(model.TimeRangeToday, time.Now().Location()) + timeRange, _ := model.GetStartOfDayForTimeRange(model.TimeRangeToday, time.Now().Location()) t.Run("get-top-channels-for-team-since", func(t *testing.T) { topChannels, err := th.App.GetTopChannelsForTeamSince(th.Context, th.BasicChannel.TeamId, th.BasicUser.Id, &model.InsightsOpts{StartUnixMilli: timeRange.UnixMilli(), Page: 0, PerPage: 5}) @@ -2576,7 +2576,7 @@ func TestGetTopChannelsForUserSince(t *testing.T) { {ID: channel5.Id, MessageCount: 2}, } - timeRange := model.StartOfDayForTimeRange(model.TimeRangeToday, time.Now().Location()) + timeRange, _ := model.GetStartOfDayForTimeRange(model.TimeRangeToday, time.Now().Location()) t.Run("get-top-channels-for-user-since", func(t *testing.T) { topChannels, err := th.App.GetTopChannelsForUserSince(th.Context, th.BasicUser.Id, "", &model.InsightsOpts{StartUnixMilli: timeRange.UnixMilli(), Page: 0, PerPage: 5}) @@ -2787,7 +2787,7 @@ func TestGetTopInactiveChannelsForTeamSince(t *testing.T) { {ID: channel2.Id, MessageCount: 6}, } - timeRange := model.StartOfDayForTimeRange(model.TimeRangeToday, time.Now().Location()) + timeRange, _ := model.GetStartOfDayForTimeRange(model.TimeRangeToday, time.Now().Location()) t.Run("get-top-channels-for-team-since", func(t *testing.T) { topChannels, err := th.App.GetTopInactiveChannelsForTeamSince(th.Context, th.BasicChannel.TeamId, th.BasicUser.Id, &model.InsightsOpts{StartUnixMilli: timeRange.UnixMilli(), Page: 0, PerPage: 5}) @@ -2885,7 +2885,7 @@ func TestGetTopInactiveChannelsForUserSince(t *testing.T) { {ID: channel2.Id, MessageCount: 6}, } - timeRange := model.StartOfDayForTimeRange(model.TimeRangeToday, time.Now().Location()) + timeRange, _ := model.GetStartOfDayForTimeRange(model.TimeRangeToday, time.Now().Location()) t.Run("get-top-channels-for-user-since", func(t *testing.T) { topChannels, err := th.App.GetTopInactiveChannelsForUserSince(th.Context, th.BasicChannel.TeamId, th.BasicUser.Id, &model.InsightsOpts{StartUnixMilli: timeRange.UnixMilli(), Page: 0, PerPage: 4}) diff --git a/app/reaction_test.go b/app/reaction_test.go index d9670e4eeb..33d747136b 100644 --- a/app/reaction_test.go +++ b/app/reaction_test.go @@ -230,7 +230,7 @@ func TestGetTopReactionsForTeamSince(t *testing.T) { expectedTopReactions[3] = &model.TopReaction{EmojiName: "sad", Count: int64(3)} expectedTopReactions[4] = &model.TopReaction{EmojiName: "happy", Count: int64(2)} - timeRange := model.StartOfDayForTimeRange(model.TimeRangeToday, time.Now().Location()) + timeRange, _ := model.GetStartOfDayForTimeRange(model.TimeRangeToday, time.Now().Location()) t.Run("get-top-reactions-for-team-since", func(t *testing.T) { topReactions, err := th.App.GetTopReactionsForTeamSince(teamId, userId, &model.InsightsOpts{StartUnixMilli: timeRange.UnixMilli(), Page: 0, PerPage: 5}) @@ -401,7 +401,7 @@ func TestGetTopReactionsForUserSince(t *testing.T) { expectedTopReactions[3] = &model.TopReaction{EmojiName: "heart", Count: int64(3)} expectedTopReactions[4] = &model.TopReaction{EmojiName: "blush", Count: int64(2)} - timeRange := model.StartOfDayForTimeRange(model.TimeRangeToday, time.Now().Location()) + timeRange, _ := model.GetStartOfDayForTimeRange(model.TimeRangeToday, time.Now().Location()) t.Run("get-top-reactions-for-user-since", func(t *testing.T) { topReactions, err := th.App.GetTopReactionsForUserSince(userId, teamId, &model.InsightsOpts{StartUnixMilli: timeRange.UnixMilli(), Page: 0, PerPage: 5}) diff --git a/i18n/en.json b/i18n/en.json index f5db7894ed..710054bf1a 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -8739,6 +8739,10 @@ "id": "model.incoming_hook.username.app_error", "translation": "Invalid username." }, + { + "id": "model.insights.get_start_of_day_for_time_range.time_range.app_error", + "translation": "Invalid time range." + }, { "id": "model.job.is_valid.create_at.app_error", "translation": "Create at must be a valid time." diff --git a/model/insights.go b/model/insights.go index 438ca9402c..d66d48c283 100644 --- a/model/insights.go +++ b/model/insights.go @@ -4,6 +4,7 @@ package model import ( + "net/http" "time" ) @@ -260,6 +261,23 @@ func StartOfDayForTimeRange(timeRange string, location *time.Location) *time.Tim return &resultTime } +// GetStartOfDayForTimeRange gets the unix start time in milliseconds from the given time range. +// Time range can be one of: "today", "7_day", or "28_day". +func GetStartOfDayForTimeRange(timeRange string, location *time.Location) (*time.Time, *AppError) { + now := time.Now().In(location) + resultTime := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, location) + switch timeRange { + case TimeRangeToday: + case TimeRange7Day: + resultTime = resultTime.Add(time.Hour * time.Duration(-144)) + case TimeRange28Day: + resultTime = resultTime.Add(time.Hour * time.Duration(-648)) + default: + return nil, NewAppError("GetStartOfDayForTimeRange", "model.insights.get_start_of_day_for_time_range.time_range.app_error", nil, "", http.StatusBadRequest) + } + return &resultTime, nil +} + // GetTopReactionListWithPagination adds a rank to each item in the given list of TopReaction and checks if there is // another page that can be fetched based on the given limit and offset. The given list of TopReaction is assumed to be // sorted by Count. Returns a TopReactionList. From b30e511ffe84c559215950f8af4493ea8c449e3f Mon Sep 17 00:00:00 2001 From: Julien Tant <785518+JulienTant@users.noreply.github.com> Date: Thu, 20 Oct 2022 12:06:34 -0700 Subject: [PATCH 60/63] Deprecate ioutil (#21457) --- app/platform/helper_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/platform/helper_test.go b/app/platform/helper_test.go index b8d2da4594..9402f8def3 100644 --- a/app/platform/helper_test.go +++ b/app/platform/helper_test.go @@ -4,7 +4,7 @@ package platform import ( - "io/ioutil" + "os" "path/filepath" "sync" "testing" @@ -127,7 +127,7 @@ func SetupWithCluster(tb testing.TB, cluster einterfaces.ClusterInterface) *Test } func setupTestHelper(dbStore store.Store, enterprise bool, includeCacheLayer bool, tb testing.TB, options ...Option) *TestHelper { - tempWorkspace, err := ioutil.TempDir("", "apptest") + tempWorkspace, err := os.MkdirTemp("", "apptest") if err != nil { panic(err) } From 2ce3e81490251c71a0e7adccb5ec5cfd3e5d2472 Mon Sep 17 00:00:00 2001 From: Ibrahim Serdar Acikgoz Date: Fri, 21 Oct 2022 12:24:36 +0300 Subject: [PATCH 61/63] move file backend to the platform service (#21365) --- app/notification_push_test.go | 9 ++++---- app/options.go | 2 +- app/platform/options.go | 8 +++++++ app/platform/service.go | 19 ++++++++++++++++ app/server.go | 15 +++--------- app/server_test.go | 43 +++++++++++++++++++---------------- 6 files changed, 58 insertions(+), 38 deletions(-) diff --git a/app/notification_push_test.go b/app/notification_push_test.go index 6dd1b5a2ff..73bf38e72b 100644 --- a/app/notification_push_test.go +++ b/app/notification_push_test.go @@ -1440,20 +1440,19 @@ func TestPushNotificationRace(t *testing.T) { Return(&model.Preference{Value: "test"}, nil) mockStore.On("Preference").Return(&mockPreferenceStore) s := &Server{ - products: make(map[string]Product), - Router: mux.NewRouter(), - filestore: &fmocks.FileBackend{}, + products: make(map[string]Product), + Router: mux.NewRouter(), } var err error s.platform, err = platform.New(platform.ServiceConfig{ ConfigStore: memoryStore, - }) + }, platform.SetFileStore(&fmocks.FileBackend{})) s.SetStore(mockStore) require.NoError(t, err) serviceMap := map[ServiceKey]any{ ConfigKey: s.platform, LicenseKey: &licenseWrapper{s}, - FilestoreKey: s.filestore, + FilestoreKey: s.FileBackend(), } ch, err := NewChannels(s, serviceMap) require.NoError(t, err) diff --git a/app/options.go b/app/options.go index 15725bf4c3..19133c42fc 100644 --- a/app/options.go +++ b/app/options.go @@ -54,7 +54,7 @@ func ConfigStore(configStore *config.Store) Option { func SetFileStore(filestore filestore.FileBackend) Option { return func(s *Server) error { - s.filestore = filestore + s.platformOptions = append(s.platformOptions, platform.SetFileStore(filestore)) return nil } } diff --git a/app/platform/options.go b/app/platform/options.go index 072914e104..65471b63f6 100644 --- a/app/platform/options.go +++ b/app/platform/options.go @@ -11,6 +11,7 @@ import ( "github.com/mattermost/mattermost-server/v6/config" "github.com/mattermost/mattermost-server/v6/einterfaces" "github.com/mattermost/mattermost-server/v6/model" + "github.com/mattermost/mattermost-server/v6/shared/filestore" "github.com/mattermost/mattermost-server/v6/shared/mlog" "github.com/mattermost/mattermost-server/v6/store" "github.com/mattermost/mattermost-server/v6/store/localcachelayer" @@ -74,6 +75,13 @@ func Config(dsn string, readOnly bool, configDefaults *model.Config) Option { } } +func SetFileStore(filestore filestore.FileBackend) Option { + return func(ps *PlatformService) error { + ps.filestore = filestore + return nil + } +} + // ConfigStore applies the given config store, typically to replace the traditional sources with a memory store for testing. func ConfigStore(configStore *config.Store) Option { return func(ps *PlatformService) error { diff --git a/app/platform/service.go b/app/platform/service.go index d2104fb2bb..6620b7f487 100644 --- a/app/platform/service.go +++ b/app/platform/service.go @@ -20,6 +20,7 @@ import ( "github.com/mattermost/mattermost-server/v6/services/cache" "github.com/mattermost/mattermost-server/v6/services/searchengine" "github.com/mattermost/mattermost-server/v6/services/searchengine/bleveengine" + "github.com/mattermost/mattermost-server/v6/shared/filestore" "github.com/mattermost/mattermost-server/v6/shared/mlog" "github.com/mattermost/mattermost-server/v6/store" "github.com/mattermost/mattermost-server/v6/store/localcachelayer" @@ -41,6 +42,8 @@ type PlatformService struct { configStore *config.Store + filestore filestore.FileBackend + cacheProvider cache.Provider statusCache cache.Cache sessionCache cache.Cache @@ -213,6 +216,18 @@ func New(sc ServiceConfig, options ...Option) (*PlatformService, error) { } } + license := ps.License() + // Step 3: Initialize filestore + if ps.filestore == nil { + insecure := ps.Config().ServiceSettings.EnableInsecureOutgoingConnections + backend, err2 := filestore.NewFileBackend(ps.Config().FileSettings.ToFileBackendSettings(license != nil && *license.Features.Compliance, insecure != nil && *insecure)) + if err2 != nil { + return nil, fmt.Errorf("failed to initialize filebackend: %w", err2) + } + + ps.filestore = backend + } + var err error ps.Store, err = ps.newStore() if err != nil { @@ -431,3 +446,7 @@ func (ps *PlatformService) GetPluginStatuses() (model.PluginStatuses, *model.App return pluginStatuses, nil } + +func (ps *PlatformService) FileBackend() filestore.FileBackend { + return ps.filestore +} diff --git a/app/server.go b/app/server.go index a4688c8b43..0216fa240f 100644 --- a/app/server.go +++ b/app/server.go @@ -137,7 +137,6 @@ type Server struct { openGraphDataCache cache.Cache clusterLeaderListenerId string loggerLicenseListenerId string - filestore filestore.FileBackend platform *platform.PlatformService platformOptions []platform.Option @@ -239,15 +238,6 @@ func NewServer(options ...Option) (*Server, error) { s.LoadLicense() } - license := s.License() - insecure := s.platform.Config().ServiceSettings.EnableInsecureOutgoingConnections - // Step 3: Initialize filestore - backend, err := filestore.NewFileBackend(s.platform.Config().FileSettings.ToFileBackendSettings(license != nil && *license.Features.Compliance, insecure != nil && *insecure)) - if err != nil { - return nil, errors.Wrap(err, "failed to initialize filebackend") - } - s.filestore = backend - s.licenseWrapper = &licenseWrapper{ srv: s, } @@ -272,7 +262,7 @@ func NewServer(options ...Option) (*Server, error) { ChannelKey: &channelsWrapper{srv: s}, ConfigKey: s.platform, LicenseKey: s.licenseWrapper, - FilestoreKey: s.filestore, + FilestoreKey: s.platform.FileBackend(), FileInfoStoreKey: &fileInfoWrapper{srv: s}, ClusterKey: s.platform, UserKey: New(ServerConnector(s.Channels())), @@ -439,6 +429,7 @@ func NewServer(options ...Option) (*Server, error) { mlog.Info("Printing current working", mlog.String("directory", pwd)) mlog.Info("Loaded config", mlog.String("source", s.platform.DescribeConfig())) + license := s.License() allowAdvancedLogging := license != nil && *license.Features.AdvancedLogging if s.Audit == nil { @@ -1425,7 +1416,7 @@ func (s *Server) SendRemoveExpiredLicenseEmail(email string, renewalLink, locale } func (s *Server) FileBackend() filestore.FileBackend { - return s.filestore + return s.platform.FileBackend() } func (s *Server) TotalWebsocketConnections() int { diff --git a/app/server_test.go b/app/server_test.go index f8135d0461..64b85ce938 100644 --- a/app/server_test.go +++ b/app/server_test.go @@ -90,28 +90,29 @@ func TestStartServerNoS3Bucket(t *testing.T) { } s3Endpoint := fmt.Sprintf("%s:%s", s3Host, s3Port) + configStore, _ := config.NewFileStore("config.json", true) + store, _ := config.NewStoreFromBacking(configStore, nil, false) + + cfg := store.Get() + cfg.FileSettings = model.FileSettings{ + DriverName: model.NewString(model.ImageDriverS3), + AmazonS3AccessKeyId: model.NewString(model.MinioAccessKey), + AmazonS3SecretAccessKey: model.NewString(model.MinioSecretKey), + AmazonS3Bucket: model.NewString("nosuchbucket"), + AmazonS3Endpoint: model.NewString(s3Endpoint), + AmazonS3Region: model.NewString(""), + AmazonS3PathPrefix: model.NewString(""), + AmazonS3SSL: model.NewBool(false), + } + *cfg.ServiceSettings.ListenAddress = ":0" + _, _, err := store.Set(cfg) + require.NoError(t, err) s, err := NewServer(func(server *Server) error { - configStore, _ := config.NewFileStore("config.json", true) - store, _ := config.NewStoreFromBacking(configStore, nil, false) - var err error - server.platform, err = platform.New(platform.ServiceConfig{ - ConfigStore: store, - }) - require.NoError(t, err) - server.platform.UpdateConfig(func(cfg *model.Config) { - cfg.FileSettings = model.FileSettings{ - DriverName: model.NewString(model.ImageDriverS3), - AmazonS3AccessKeyId: model.NewString(model.MinioAccessKey), - AmazonS3SecretAccessKey: model.NewString(model.MinioSecretKey), - AmazonS3Bucket: model.NewString("nosuchbucket"), - AmazonS3Endpoint: model.NewString(s3Endpoint), - AmazonS3Region: model.NewString(""), - AmazonS3PathPrefix: model.NewString(""), - AmazonS3SSL: model.NewBool(false), - } - *cfg.ServiceSettings.ListenAddress = ":0" - }) + var err2 error + server.platform, err2 = platform.New(platform.ServiceConfig{}, platform.ConfigStore(store)) + require.NoError(t, err2) + return nil }) require.NoError(t, err) @@ -120,6 +121,8 @@ func TestStartServerNoS3Bucket(t *testing.T) { defer s.Shutdown() // ensure that a new bucket was created + require.IsType(t, &filestore.S3FileBackend{}, s.FileBackend()) + err = s.FileBackend().(*filestore.S3FileBackend).TestConnection() require.NoError(t, err) } From 0dc8b7b9dd531a0e06a665d4264cbb5beaaa8c5c Mon Sep 17 00:00:00 2001 From: Scott Bishel Date: Fri, 21 Oct 2022 09:54:17 -0600 Subject: [PATCH 62/63] update prepackage boards to v7.4.3 (#21481) --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 43ccf306a5..85b7c222ef 100644 --- a/Makefile +++ b/Makefile @@ -160,7 +160,7 @@ PLUGIN_PACKAGES += mattermost-plugin-jira-v2.4.0 PLUGIN_PACKAGES += mattermost-plugin-nps-v1.2.0 PLUGIN_PACKAGES += mattermost-plugin-welcomebot-v1.2.0 PLUGIN_PACKAGES += mattermost-plugin-zoom-v1.6.0 -PLUGIN_PACKAGES += focalboard-v7.4.2 +PLUGIN_PACKAGES += focalboard-v7.4.3 PLUGIN_PACKAGES += mattermost-plugin-apps-v1.1.0 # Prepares the enterprise build if exists. The IGNORE stuff is a hack to get the Makefile to execute the commands outside a target From 6ac0faf99ea6de16f597fc4cf1a1b341826d5784 Mon Sep 17 00:00:00 2001 From: Scott Bishel Date: Fri, 21 Oct 2022 10:10:45 -0600 Subject: [PATCH 63/63] Display board statistics (#21433) * prepackage boards v7.4.2 * allow plugin to call HasPermissionTo Co-authored-by: Mattermod --- app/permissions.go | 4 ++++ product/api.go | 1 + 2 files changed, 5 insertions(+) diff --git a/app/permissions.go b/app/permissions.go index a992d0a952..2b34b85bb3 100644 --- a/app/permissions.go +++ b/app/permissions.go @@ -29,6 +29,10 @@ type permissionsServiceWrapper struct { app AppIface } +func (s *permissionsServiceWrapper) HasPermissionTo(userID string, permission *model.Permission) bool { + return s.app.HasPermissionTo(userID, permission) +} + func (s *permissionsServiceWrapper) HasPermissionToTeam(userID string, teamID string, permission *model.Permission) bool { return s.app.HasPermissionToTeam(userID, teamID, permission) } diff --git a/product/api.go b/product/api.go index 4b4ec8a93c..88eb9744e4 100644 --- a/product/api.go +++ b/product/api.go @@ -39,6 +39,7 @@ type PostService interface { // // The service shall be registered via app.PermissionKey service key. type PermissionService interface { + HasPermissionTo(userID string, permission *model.Permission) bool HasPermissionToTeam(userID, teamID string, permission *model.Permission) bool HasPermissionToChannel(askingUserID string, channelID string, permission *model.Permission) bool }