Add the ability to patch channel autotranslations (#35078)

* Add the ability to patch channel autotranslations

* Fix lint

* Update docs

* Fix CI

* Fix CI

* Fix mmctl test

* Check whether the channel is translated for the user when checking user enabled

* Fix wrong uses of patch acrros e2e and frontend

* Fix test

* Fix wording

* Fix tests and column name

* Move group constrained test so they don't mess with the basic entities

* Fix patch sending too much information
This commit is contained in:
Daniel Espino García
2026-02-06 18:19:06 +01:00
committed by GitHub
parent f6574143a8
commit 2bd29c0359
43 changed files with 919 additions and 604 deletions
+41 -9
View File
@@ -606,18 +606,36 @@
summary: Patch a channel
description: >
Partially update a channel by providing only the fields you want to
update. Omitted fields will not be updated. The fields that can be
updated are defined in the request body, all other provided fields will
be ignored.
update. Omitted fields will not be updated. At least one of the allowed
fields must be provided.
**Public and private channels:** Can update `name`, `display_name`,
`purpose`, `header`, `group_constrained`, `autotranslation`, and
`banner_info` (subject to permissions and channel type).
**Direct and group message channels:** Only `header` and (when not
restricted by config) `autotranslation` can be updated; the caller
must be a channel member. Updating `name`, `display_name`, or `purpose`
is not allowed.
The default channel (e.g. Town Square) cannot have its `name` changed.
##### Permissions
If updating a public channel, `manage_public_channel_members` permission is required. If updating a private channel, `manage_private_channel_members` permission is required.
- **Public channel:** For property updates (name, display_name, purpose, header, group_constrained),
`manage_public_channel_properties` is required. For `autotranslation`, `manage_public_channel_auto_translation`
is required. For `banner_info`, `manage_public_channel_banner` is required (Channel Banner feature and
Enterprise license required).
- **Private channel:** For property updates, `manage_private_channel_properties` is required. For
`autotranslation`, `manage_private_channel_auto_translation` is required. For `banner_info`,
`manage_private_channel_banner` is required (Channel Banner feature and Enterprise license required).
- **Direct or group message channel:** Must be a member of the channel; only `header` and (when allowed)
`autotranslation` can be updated.
operationId: PatchChannel
parameters:
- name: channel_id
in: path
description: Channel GUID
description: Channel ID
required: true
schema:
type: string
@@ -630,20 +648,34 @@
name:
type: string
description: The unique handle for the channel, will be present in the
channel URL
channel URL. Cannot be updated for direct or group message channels.
Cannot be changed for the default channel (e.g. Town Square).
display_name:
type: string
description: The non-unique UI name for the channel
description: The non-unique UI name for the channel. Cannot be updated
for direct or group message channels.
purpose:
type: string
description: A short description of the purpose of the channel
description: A short description of the purpose of the channel. Cannot
be updated for direct or group message channels.
header:
type: string
description: Markdown-formatted text to display in the header of the
channel
group_constrained:
type: boolean
description: When true, only members of the linked LDAP groups can join
the channel. Only applicable to public and private channels.
autotranslation:
type: boolean
description: Enable or disable automatic message translation in the
channel. Requires the auto-translation feature and appropriate
channel permission. May be restricted for direct and group message
channels by server configuration.
banner_info:
$ref: "#/components/schemas/ChannelBanner"
description: Channel object to be updated
description: Channel patch object; include only the fields to update. At least
one field must be provided.
required: true
responses:
"200":
@@ -207,7 +207,6 @@ describe('Channel Info RHS', () => {
cy.get('#channel-info-btn').click();
cy.apiPatchChannel(testChannel.id, {
...testChannel,
purpose: 'purpose for the tests',
}).then(() => {
cy.uiGetRHS().findByText('purpose for the tests').should('be.visible');
@@ -221,7 +220,6 @@ describe('Channel Info RHS', () => {
cy.get('#channel-info-btn').click();
cy.apiPatchChannel(testChannel.id, {
...testChannel,
header: 'header for the tests',
}).then(() => {
cy.uiGetRHS().findByText('header for the tests').should('be.visible');
@@ -433,7 +431,6 @@ describe('Channel Info RHS', () => {
cy.get('#channel-info-btn').click();
cy.apiPatchChannel(groupChannel.id, {
...groupChannel,
header: 'header for the tests',
}).then(() => {
cy.uiGetRHS().findByText('header for the tests').should('be.visible');
@@ -527,7 +524,6 @@ describe('Channel Info RHS', () => {
cy.get('#channel-info-btn').click();
cy.apiPatchChannel(directChannel.id, {
...directChannel,
header: 'header for the tests',
}).then(() => {
cy.uiGetRHS().findByText('header for the tests').should('be.visible');
File diff suppressed because one or more lines are too long
@@ -854,5 +854,6 @@ const defaultServerConfig: AdminConfig = {
Agents: {
LLMServiceID: '',
},
RestrictDMAndGM: false,
},
};
+40 -6
View File
@@ -345,17 +345,46 @@ func patchChannel(c *Context, w http.ResponseWriter, r *http.Request) {
model.AddEventParameterAuditableToAuditRec(auditRec, "channel", patch)
auditRec.AddEventPriorState(oldChannel)
updatingProperties := patch.DisplayName != nil || patch.Name != nil || patch.Header != nil || patch.Purpose != nil || patch.GroupConstrained != nil
updatingAutoTranslation := patch.AutoTranslation != nil
if !updatingProperties && !updatingAutoTranslation && patch.BannerInfo == nil {
c.Err = model.NewAppError("patchChannel", "api.channel.patch_update_channel.no_changes.app_error", nil, "", http.StatusBadRequest)
return
}
if updatingAutoTranslation && (c.App.AutoTranslation() == nil || !c.App.AutoTranslation().IsFeatureAvailable()) {
c.Err = model.NewAppError("patchChannel", "api.channel.patch_update_channel.feature_not_available.app_error", nil, "", http.StatusForbidden)
return
}
switch oldChannel.Type {
case model.ChannelTypeOpen:
if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionManagePublicChannelProperties); !ok {
c.SetPermissionError(model.PermissionManagePublicChannelProperties)
return
if updatingProperties {
if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionManagePublicChannelProperties); !ok {
c.SetPermissionError(model.PermissionManagePublicChannelProperties)
return
}
}
if updatingAutoTranslation {
if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionManagePublicChannelAutoTranslation); !ok {
c.SetPermissionError(model.PermissionManagePublicChannelAutoTranslation)
return
}
}
case model.ChannelTypePrivate:
if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionManagePrivateChannelProperties); !ok {
c.SetPermissionError(model.PermissionManagePrivateChannelProperties)
return
if updatingProperties {
if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionManagePrivateChannelProperties); !ok {
c.SetPermissionError(model.PermissionManagePrivateChannelProperties)
return
}
}
if updatingAutoTranslation {
if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionManagePrivateChannelAutoTranslation); !ok {
c.SetPermissionError(model.PermissionManagePrivateChannelAutoTranslation)
return
}
}
case model.ChannelTypeGroup, model.ChannelTypeDirect:
@@ -369,6 +398,11 @@ func patchChannel(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
if updatingAutoTranslation && *c.App.Config().AutoTranslationSettings.RestrictDMAndGM {
c.Err = model.NewAppError("patchChannel", "api.channel.patch_update_channel.auto_translation_restricted.app_error", nil, "", http.StatusForbidden)
return
}
default:
c.Err = model.NewAppError("patchChannel", "api.channel.patch_update_channel.forbidden.app_error", nil, "", http.StatusForbidden)
return
+439 -67
View File
@@ -6,6 +6,7 @@ package api4
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
@@ -482,76 +483,10 @@ func TestUpdateChannel(t *testing.T) {
})
}
func TestPatchChannel(t *testing.T) {
func TestPatchChannelGroupConstrained(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
client := th.Client
team := th.BasicTeam
t.Run("should be unable to apply a null patch", func(t *testing.T) {
var nullPatch *model.ChannelPatch
_, nullResp, err := client.PatchChannel(context.Background(), th.BasicChannel.Id, nullPatch)
require.Error(t, err)
CheckBadRequestStatus(t, nullResp)
})
t.Run("should be able to patch values", func(t *testing.T) {
patch := &model.ChannelPatch{
Name: new(string),
DisplayName: new(string),
Header: new(string),
Purpose: new(string),
}
*patch.Name = model.NewId()
*patch.DisplayName = model.NewId()
*patch.Header = model.NewId()
*patch.Purpose = model.NewId()
channel, _, err := client.PatchChannel(context.Background(), th.BasicChannel.Id, patch)
require.NoError(t, err)
require.Equal(t, *patch.Name, channel.Name, "do not match")
require.Equal(t, *patch.DisplayName, channel.DisplayName, "do not match")
require.Equal(t, *patch.Header, channel.Header, "do not match")
require.Equal(t, *patch.Purpose, channel.Purpose, "do not match")
})
t.Run("should be able to patch with no name", func(t *testing.T) {
channel := &model.Channel{
DisplayName: GenerateTestChannelName(),
Name: GenerateTestChannelName(),
Type: model.ChannelTypeOpen,
TeamId: team.Id,
}
var err error
channel, _, err = client.CreateChannel(context.Background(), channel)
require.NoError(t, err)
patch := &model.ChannelPatch{
Header: new(string),
Purpose: new(string),
}
oldName := channel.Name
patchedChannel, _, err := client.PatchChannel(context.Background(), channel.Id, patch)
require.NoError(t, err)
require.Equal(t, oldName, patchedChannel.Name, "should not have updated")
})
t.Run("Test updating default channel's name and returns error", func(t *testing.T) {
// Test updating default channel's name and returns error
defaultChannel, appErr := th.App.GetChannelByName(th.Context, model.DefaultChannelName, team.Id, false)
require.Nil(t, appErr)
defaultChannelPatch := &model.ChannelPatch{
Name: new(string),
}
*defaultChannelPatch.Name = "testing"
_, resp, err := client.PatchChannel(context.Background(), defaultChannel.Id, defaultChannelPatch)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
})
t.Run("Test GroupConstrained flag", func(t *testing.T) {
// Test GroupConstrained flag
@@ -575,6 +510,9 @@ func TestPatchChannel(t *testing.T) {
user := th.CreateUser(t)
_, _, err = client.Login(context.Background(), user.Email, user.Password)
require.NoError(t, err)
patch.GroupConstrained = model.NewPointer(false)
_, resp, err = client.PatchChannel(context.Background(), th.BasicChannel.Id, patch)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
@@ -753,6 +691,78 @@ func TestPatchChannel(t *testing.T) {
}
}
})
}
func TestPatchChannel(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
client := th.Client
team := th.BasicTeam
t.Run("should be unable to apply a null patch", func(t *testing.T) {
var nullPatch *model.ChannelPatch
_, nullResp, err := client.PatchChannel(context.Background(), th.BasicChannel.Id, nullPatch)
require.Error(t, err)
CheckBadRequestStatus(t, nullResp)
})
t.Run("should be able to patch values", func(t *testing.T) {
patch := &model.ChannelPatch{
Name: new(string),
DisplayName: new(string),
Header: new(string),
Purpose: new(string),
}
*patch.Name = model.NewId()
*patch.DisplayName = model.NewId()
*patch.Header = model.NewId()
*patch.Purpose = model.NewId()
channel, _, err := client.PatchChannel(context.Background(), th.BasicChannel.Id, patch)
require.NoError(t, err)
require.Equal(t, *patch.Name, channel.Name, "do not match")
require.Equal(t, *patch.DisplayName, channel.DisplayName, "do not match")
require.Equal(t, *patch.Header, channel.Header, "do not match")
require.Equal(t, *patch.Purpose, channel.Purpose, "do not match")
})
t.Run("should be able to patch with no name", func(t *testing.T) {
channel := &model.Channel{
DisplayName: GenerateTestChannelName(),
Name: GenerateTestChannelName(),
Type: model.ChannelTypeOpen,
TeamId: team.Id,
}
var err error
channel, _, err = client.CreateChannel(context.Background(), channel)
require.NoError(t, err)
patch := &model.ChannelPatch{
Header: new(string),
Purpose: new(string),
}
oldName := channel.Name
patchedChannel, _, err := client.PatchChannel(context.Background(), channel.Id, patch)
require.NoError(t, err)
require.Equal(t, oldName, patchedChannel.Name, "should not have updated")
})
t.Run("Test updating default channel's name and returns error", func(t *testing.T) {
// Test updating default channel's name and returns error
defaultChannel, appErr := th.App.GetChannelByName(th.Context, model.DefaultChannelName, team.Id, false)
require.Nil(t, appErr)
defaultChannelPatch := &model.ChannelPatch{
Name: new(string),
}
*defaultChannelPatch.Name = "testing"
_, resp, err := client.PatchChannel(context.Background(), defaultChannel.Id, defaultChannelPatch)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
})
t.Run("Test updating the header of someone else's GM channel", func(t *testing.T) {
// Test updating the header of someone else's GM channel.
@@ -1137,6 +1147,368 @@ func TestPatchChannel(t *testing.T) {
CheckBadRequestStatus(t, resp)
require.Nil(t, patchedChannel)
})
t.Run("Patch channel with no changes returns 400", func(t *testing.T) {
_, err := client.Logout(context.Background())
require.NoError(t, err)
th.LoginBasic(t)
patch := &model.ChannelPatch{}
_, resp, err := client.PatchChannel(context.Background(), th.BasicChannel.Id, patch)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
})
t.Run("Patch channel with autotranslation when feature is available properly updates the channel for admins", func(t *testing.T) {
mockAutoTranslation := &einterfacesmocks.AutoTranslationInterface{}
mockAutoTranslation.On("IsFeatureAvailable").Return(true)
mockAutoTranslation.On("IsChannelEnabled", mock.Anything).Return(true, nil)
mockAutoTranslation.On("Translate", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, nil)
originalAutoTranslation := th.Server.AutoTranslation
th.Server.AutoTranslation = mockAutoTranslation
defer func() {
th.Server.AutoTranslation = originalAutoTranslation
}()
_, err := th.SystemAdminClient.Logout(context.Background())
require.NoError(t, err)
th.LoginSystemAdmin(t)
patch := &model.ChannelPatch{
AutoTranslation: model.NewPointer(true),
}
_, resp, err := th.SystemAdminClient.PatchChannel(context.Background(), th.BasicChannel.Id, patch)
require.NoError(t, err)
CheckOKStatus(t, resp)
patchedChannel, appErr := th.App.GetChannel(th.Context, th.BasicChannel.Id)
require.Nil(t, appErr)
require.True(t, patchedChannel.AutoTranslation)
patch = &model.ChannelPatch{
AutoTranslation: model.NewPointer(false),
}
_, resp, err = th.SystemAdminClient.PatchChannel(context.Background(), th.BasicChannel.Id, patch)
require.NoError(t, err)
CheckOKStatus(t, resp)
patchedChannel, appErr = th.App.GetChannel(th.Context, th.BasicChannel.Id)
require.Nil(t, appErr)
require.False(t, patchedChannel.AutoTranslation)
})
t.Run("Patch channel with autotranslation when feature is available properly updates the channel for users only with the proper permissions", func(t *testing.T) {
mockAutoTranslation := &einterfacesmocks.AutoTranslationInterface{}
mockAutoTranslation.On("IsFeatureAvailable").Return(true)
mockAutoTranslation.On("IsChannelEnabled", mock.Anything).Return(true, nil)
mockAutoTranslation.On("Translate", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, nil)
originalAutoTranslation := th.Server.AutoTranslation
th.Server.AutoTranslation = mockAutoTranslation
defer func() {
th.Server.AutoTranslation = originalAutoTranslation
}()
_, err := client.Logout(context.Background())
require.NoError(t, err)
th.LoginBasic(t)
privateChannel := th.CreateChannelWithClient(t, th.SystemAdminClient, model.ChannelTypePrivate)
th.AddUserToChannel(t, th.BasicUser, privateChannel)
patch := &model.ChannelPatch{
AutoTranslation: model.NewPointer(true),
}
_, resp, err := client.PatchChannel(context.Background(), th.BasicChannel.Id, patch)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
_, resp, err = client.PatchChannel(context.Background(), privateChannel.Id, patch)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
th.AddPermissionToRole(t, model.PermissionManagePrivateChannelAutoTranslation.Id, model.SystemUserRoleId)
defer th.RemovePermissionFromRole(t, model.PermissionManagePrivateChannelAutoTranslation.Id, model.SystemUserRoleId)
_, resp, err = client.PatchChannel(context.Background(), th.BasicChannel.Id, patch)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
_, _, err = client.PatchChannel(context.Background(), privateChannel.Id, patch)
require.NoError(t, err)
patchedChannel, appErr := th.App.GetChannel(th.Context, privateChannel.Id)
require.Nil(t, appErr)
require.True(t, patchedChannel.AutoTranslation)
th.AddPermissionToRole(t, model.PermissionManagePublicChannelAutoTranslation.Id, model.SystemUserRoleId)
defer th.RemovePermissionFromRole(t, model.PermissionManagePublicChannelAutoTranslation.Id, model.SystemUserRoleId)
_, _, err = client.PatchChannel(context.Background(), th.BasicChannel.Id, patch)
require.NoError(t, err)
patchedChannel, appErr = th.App.GetChannel(th.Context, privateChannel.Id)
require.Nil(t, appErr)
require.True(t, patchedChannel.AutoTranslation)
})
t.Run("Patch channel with AutoTranslation when feature not available returns 403", func(t *testing.T) {
mockAutoTranslation := &einterfacesmocks.AutoTranslationInterface{}
mockAutoTranslation.On("IsFeatureAvailable").Return(false)
mockAutoTranslation.On("IsChannelEnabled", mock.Anything).Return(true, nil)
mockAutoTranslation.On("Translate", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, nil)
originalAutoTranslation := th.Server.AutoTranslation
th.Server.AutoTranslation = mockAutoTranslation
defer func() {
th.Server.AutoTranslation = originalAutoTranslation
}()
_, err := client.Logout(context.Background())
require.NoError(t, err)
th.LoginBasic(t)
patch := &model.ChannelPatch{
AutoTranslation: model.NewPointer(true),
}
_, resp, err := client.PatchChannel(context.Background(), th.BasicChannel.Id, patch)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
var appErr *model.AppError
require.True(t, errors.As(err, &appErr))
require.Contains(t, []string{"api.channel.patch_update_channel.feature_not_available.app_error", "api.channel.patch_update_channel.auto_translation_restricted.app_error"}, appErr.Id)
})
t.Run("Patch channel with autotranslation on DM is only available for members", func(t *testing.T) {
mockAutoTranslation := &einterfacesmocks.AutoTranslationInterface{}
mockAutoTranslation.On("IsFeatureAvailable").Return(true)
mockAutoTranslation.On("IsChannelEnabled", mock.Anything).Return(true, nil)
mockAutoTranslation.On("Translate", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, nil)
originalAutoTranslation := th.Server.AutoTranslation
th.Server.AutoTranslation = mockAutoTranslation
defer func() {
th.Server.AutoTranslation = originalAutoTranslation
}()
_, err := client.Logout(context.Background())
require.NoError(t, err)
th.LoginBasic(t)
dmChannel, resp, err := client.CreateDirectChannel(context.Background(), th.BasicUser.Id, th.BasicUser2.Id)
require.NoError(t, err)
CheckCreatedStatus(t, resp)
nonMemberDmChannel, resp, err := th.SystemAdminClient.CreateDirectChannel(context.Background(), th.BasicUser2.Id, th.SystemAdminUser.Id)
require.NoError(t, err)
CheckCreatedStatus(t, resp)
patch := &model.ChannelPatch{
AutoTranslation: model.NewPointer(true),
}
_, resp, err = client.PatchChannel(context.Background(), dmChannel.Id, patch)
require.NoError(t, err)
CheckOKStatus(t, resp)
patchedChannel, appErr := th.App.GetChannel(th.Context, dmChannel.Id)
require.Nil(t, appErr)
require.True(t, patchedChannel.AutoTranslation)
_, resp, err = client.PatchChannel(context.Background(), nonMemberDmChannel.Id, patch)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("Patch channel with autotranslation on GM is only available for members", func(t *testing.T) {
mockAutoTranslation := &einterfacesmocks.AutoTranslationInterface{}
mockAutoTranslation.On("IsFeatureAvailable").Return(true)
mockAutoTranslation.On("IsChannelEnabled", mock.Anything).Return(true, nil)
mockAutoTranslation.On("Translate", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, nil)
originalAutoTranslation := th.Server.AutoTranslation
th.Server.AutoTranslation = mockAutoTranslation
defer func() {
th.Server.AutoTranslation = originalAutoTranslation
}()
_, err := client.Logout(context.Background())
require.NoError(t, err)
th.LoginBasic(t)
user3 := th.CreateUser(t)
gmChannel, resp, err := client.CreateGroupChannel(context.Background(), []string{th.BasicUser.Id, th.BasicUser2.Id, user3.Id})
require.NoError(t, err)
CheckCreatedStatus(t, resp)
nonMemberGmChannel, resp, err := th.SystemAdminClient.CreateGroupChannel(context.Background(), []string{th.BasicUser2.Id, th.SystemAdminUser.Id, user3.Id})
require.NoError(t, err)
CheckCreatedStatus(t, resp)
patch := &model.ChannelPatch{
AutoTranslation: model.NewPointer(true),
}
_, resp, err = client.PatchChannel(context.Background(), gmChannel.Id, patch)
require.NoError(t, err)
CheckOKStatus(t, resp)
patchedChannel, appErr := th.App.GetChannel(th.Context, gmChannel.Id)
require.Nil(t, appErr)
require.True(t, patchedChannel.AutoTranslation)
_, resp, err = client.PatchChannel(context.Background(), nonMemberGmChannel.Id, patch)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("Patch DM with AutoTranslation when RestrictDMAndGM is true returns 403", func(t *testing.T) {
mockAutoTranslation := &einterfacesmocks.AutoTranslationInterface{}
mockAutoTranslation.On("IsFeatureAvailable").Return(true)
mockAutoTranslation.On("IsChannelEnabled", mock.Anything).Return(true, nil)
mockAutoTranslation.On("Translate", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, nil)
originalAutoTranslation := th.Server.AutoTranslation
th.Server.AutoTranslation = mockAutoTranslation
defer func() {
th.Server.AutoTranslation = originalAutoTranslation
}()
_, err := client.Logout(context.Background())
require.NoError(t, err)
th.LoginBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.AutoTranslationSettings.RestrictDMAndGM = true
})
defer th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.AutoTranslationSettings.RestrictDMAndGM = false
})
dmChannel, resp, err := client.CreateDirectChannel(context.Background(), th.BasicUser.Id, th.BasicUser2.Id)
require.NoError(t, err)
CheckCreatedStatus(t, resp)
patch := &model.ChannelPatch{
AutoTranslation: model.NewPointer(true),
}
_, resp, err = client.PatchChannel(context.Background(), dmChannel.Id, patch)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
// May be feature_not_available when AutoTranslation is nil, or auto_translation_restricted when RestrictDMAndGM applies
var appErr *model.AppError
require.True(t, errors.As(err, &appErr))
require.Contains(t, []string{"api.channel.patch_update_channel.feature_not_available.app_error", "api.channel.patch_update_channel.auto_translation_restricted.app_error"}, appErr.Id)
})
t.Run("Patch GM with AutoTranslation when RestrictDMAndGM is true returns 403", func(t *testing.T) {
mockAutoTranslation := &einterfacesmocks.AutoTranslationInterface{}
mockAutoTranslation.On("IsFeatureAvailable").Return(true)
mockAutoTranslation.On("IsChannelEnabled", mock.Anything).Return(true, nil)
mockAutoTranslation.On("Translate", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, nil)
originalAutoTranslation := th.Server.AutoTranslation
th.Server.AutoTranslation = mockAutoTranslation
defer func() {
th.Server.AutoTranslation = originalAutoTranslation
}()
_, err := client.Logout(context.Background())
require.NoError(t, err)
th.LoginBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.AutoTranslationSettings.RestrictDMAndGM = true
})
defer th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.AutoTranslationSettings.RestrictDMAndGM = false
})
user3 := th.CreateUser(t)
gmChannel, resp, err := client.CreateGroupChannel(context.Background(), []string{th.BasicUser.Id, th.BasicUser2.Id, user3.Id})
require.NoError(t, err)
CheckCreatedStatus(t, resp)
patch := &model.ChannelPatch{
AutoTranslation: model.NewPointer(true),
}
_, resp, err = client.PatchChannel(context.Background(), gmChannel.Id, patch)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
// May be feature_not_available when AutoTranslation is nil, or auto_translation_restricted when RestrictDMAndGM applies
var appErr *model.AppError
require.True(t, errors.As(err, &appErr))
require.Contains(t, []string{"api.channel.patch_update_channel.feature_not_available.app_error", "api.channel.patch_update_channel.auto_translation_restricted.app_error"}, appErr.Id)
})
t.Run("Mixed patch only gets through if all permissions are met", func(t *testing.T) {
mockAutoTranslation := &einterfacesmocks.AutoTranslationInterface{}
mockAutoTranslation.On("IsFeatureAvailable").Return(true)
mockAutoTranslation.On("IsChannelEnabled", mock.Anything).Return(true, nil)
mockAutoTranslation.On("Translate", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, nil)
originalAutoTranslation := th.Server.AutoTranslation
th.Server.AutoTranslation = mockAutoTranslation
defer func() {
th.Server.AutoTranslation = originalAutoTranslation
}()
_, err := client.Logout(context.Background())
require.NoError(t, err)
th.LoginBasic(t)
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
defer func() {
appErr := th.App.Srv().RemoveLicense()
require.Nil(t, appErr)
}()
// Mixed patch (channel property + AutoTranslation) fails when user lacks AutoTranslation permission
newHeader := "mixed patch header"
mixedPatch := &model.ChannelPatch{
Header: &newHeader,
AutoTranslation: model.NewPointer(true),
BannerInfo: &model.ChannelBannerInfo{
Enabled: model.NewPointer(false),
Text: model.NewPointer("mixed patch banner"),
},
}
// Permissions missing: AutoTranslation, BannerInfo
_, resp, err := client.PatchChannel(context.Background(), th.BasicChannel.Id, mixedPatch)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
// Permissions missing: Channel properties
th.AddPermissionToRole(t, model.PermissionManagePublicChannelAutoTranslation.Id, model.SystemUserRoleId)
defer th.RemovePermissionFromRole(t, model.PermissionManagePublicChannelAutoTranslation.Id, model.SystemUserRoleId)
th.AddPermissionToRole(t, model.PermissionManagePublicChannelBanner.Id, model.SystemUserRoleId)
defer th.RemovePermissionFromRole(t, model.PermissionManagePublicChannelBanner.Id, model.SystemUserRoleId)
th.RemovePermissionFromRole(t, model.PermissionManagePublicChannelProperties.Id, model.ChannelUserRoleId)
defer th.AddPermissionToRole(t, model.PermissionManagePublicChannelProperties.Id, model.ChannelUserRoleId)
_, resp, err = client.PatchChannel(context.Background(), th.BasicChannel.Id, mixedPatch)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
// Permissions missing: AutoTranslation
th.AddPermissionToRole(t, model.PermissionManagePublicChannelProperties.Id, model.ChannelUserRoleId)
th.RemovePermissionFromRole(t, model.PermissionManagePublicChannelAutoTranslation.Id, model.SystemUserRoleId)
_, resp, err = client.PatchChannel(context.Background(), th.BasicChannel.Id, mixedPatch)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
// Permission missing: BannerInfo
th.AddPermissionToRole(t, model.PermissionManagePublicChannelAutoTranslation.Id, model.SystemUserRoleId)
th.RemovePermissionFromRole(t, model.PermissionManagePublicChannelBanner.Id, model.SystemUserRoleId)
_, resp, err = client.PatchChannel(context.Background(), th.BasicChannel.Id, mixedPatch)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
// No missing permissions
th.AddPermissionToRole(t, model.PermissionManagePublicChannelBanner.Id, model.SystemUserRoleId)
patchedChannel, resp, err := client.PatchChannel(context.Background(), th.BasicChannel.Id, mixedPatch)
require.NoError(t, err)
CheckOKStatus(t, resp)
require.Equal(t, newHeader, patchedChannel.Header)
require.True(t, patchedChannel.AutoTranslation)
})
}
func TestCanEditChannelBanner(t *testing.T) {
+2
View File
@@ -148,6 +148,8 @@ func TestDoAdvancedPermissionsMigration(t *testing.T) {
model.PermissionManagePublicChannelBanner.Id,
model.PermissionManagePrivateChannelBanner.Id,
model.PermissionManageChannelAccessRules.Id,
model.PermissionManagePublicChannelAutoTranslation.Id,
model.PermissionManagePrivateChannelAutoTranslation.Id,
},
"team_user": {
model.PermissionListTeamChannels.Id,
+38
View File
@@ -948,6 +948,7 @@ func (a *App) PatchChannel(rctx request.CTX, channel *model.Channel, patch *mode
oldChannelDisplayName := channel.DisplayName
oldChannelHeader := channel.Header
oldChannelPurpose := channel.Purpose
oldChannelAutotranslation := channel.AutoTranslation
channel.Patch(patch)
a.handleChannelCategoryName(channel)
@@ -976,6 +977,12 @@ func (a *App) PatchChannel(rctx request.CTX, channel *model.Channel, patch *mode
}
}
if channel.AutoTranslation != oldChannelAutotranslation {
if err = a.postUpdateChannelAutotranslationMessage(rctx, userID, channel, channel.AutoTranslation); err != nil {
rctx.Logger().Warn(err.Error())
}
}
return channel, nil
}
@@ -1983,6 +1990,37 @@ func (a *App) PostUpdateChannelPurposeMessage(rctx request.CTX, userID string, c
return nil
}
func (a *App) postUpdateChannelAutotranslationMessage(rctx request.CTX, userID string, channel *model.Channel, newChannelAutotranslation bool) *model.AppError {
user, err := a.Srv().Store().User().Get(context.Background(), userID)
if err != nil {
return model.NewAppError("PostUpdateChannelAutotranslationMessage", "api.channel.post_update_channel_autotranslation_message.retrieve_user.error", nil, "", http.StatusBadRequest).Wrap(err)
}
var message string
if newChannelAutotranslation {
message = fmt.Sprintf(i18n.T("api.channel.post_update_channel_autotranslation_message.enabled"), user.Username)
} else {
message = fmt.Sprintf(i18n.T("api.channel.post_update_channel_autotranslation_message.disabled"), user.Username)
}
post := &model.Post{
ChannelId: channel.Id,
Message: message,
Type: model.PostTypeAutotranslationChange,
UserId: userID,
Props: model.StringInterface{
"username": user.Username,
"enabled": newChannelAutotranslation,
},
}
if _, _, err := a.CreatePost(rctx, post, channel, model.CreatePostFlags{SetOnline: true}); err != nil {
return model.NewAppError("PostUpdateChannelAutotranslationMessage", "api.channel.post_update_channel_autotranslation_message.create_post.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) PostUpdateChannelDisplayNameMessage(rctx request.CTX, userID string, channel *model.Channel, oldChannelDisplayName, newChannelDisplayName string) *model.AppError {
user, err := a.Srv().Store().User().Get(context.Background(), userID)
if err != nil {
+34
View File
@@ -3746,6 +3746,40 @@ func TestPatchChannel(t *testing.T) {
*cfg.TeamSettings.RestrictDirectMessage = model.DirectMessageAny
})
})
t.Run("Patch channel with autotranslations post a message to the channel", func(t *testing.T) {
channel := th.createChannel(t, th.BasicTeam, model.ChannelTypeOpen)
patch := &model.ChannelPatch{
AutoTranslation: model.NewPointer(true),
}
patchedChannel, appErr := th.App.PatchChannel(th.Context, channel, patch, channel.CreatorId)
require.Nil(t, appErr)
require.True(t, patchedChannel.AutoTranslation)
posts, appErr := th.App.GetPosts(th.Context, channel.Id, 0, 1)
require.Nil(t, appErr)
require.NotNil(t, posts)
systemPost := posts.Posts[posts.Order[0]]
require.Equal(t, model.PostTypeAutotranslationChange, systemPost.Type)
require.Equal(t, th.BasicUser.Username, systemPost.GetProp("username"))
require.Equal(t, true, systemPost.GetProp("enabled"))
patch.AutoTranslation = model.NewPointer(false)
patchedChannel, appErr = th.App.PatchChannel(th.Context, channel, patch, channel.CreatorId)
require.Nil(t, appErr)
require.False(t, patchedChannel.AutoTranslation)
posts, appErr = th.App.GetPosts(th.Context, channel.Id, 0, 1)
require.Nil(t, appErr)
require.NotNil(t, posts)
systemPost = posts.Posts[posts.Order[0]]
require.Equal(t, model.PostTypeAutotranslationChange, systemPost.Type)
require.Equal(t, th.BasicUser.Username, systemPost.GetProp("username"))
require.Equal(t, false, systemPost.GetProp("enabled"))
})
}
func TestCreateChannelWithCategorySorting(t *testing.T) {
+77 -58
View File
@@ -21,64 +21,66 @@ type permissionTransformation struct {
type permissionsMap []permissionTransformation
const (
PermissionManageSystem = "manage_system"
PermissionManageTeam = "manage_team"
PermissionManageEmojis = "manage_emojis"
PermissionManageOthersEmojis = "manage_others_emojis"
PermissionCreateEmojis = "create_emojis"
PermissionDeleteEmojis = "delete_emojis"
PermissionDeleteOthersEmojis = "delete_others_emojis"
PermissionManageWebhooks = "manage_webhooks"
PermissionManageOthersWebhooks = "manage_others_webhooks"
PermissionManageIncomingWebhooks = "manage_incoming_webhooks"
PermissionManageOwnIncomingWebhooks = "manage_own_incoming_webhooks"
PermissionManageOthersIncomingWebhooks = "manage_others_incoming_webhooks"
PermissionManageOutgoingWebhooks = "manage_outgoing_webhooks"
PermissionManageOwnOutgoingWebhooks = "manage_own_outgoing_webhooks"
PermissionManageOthersOutgoingWebhooks = "manage_others_outgoing_webhooks"
PermissionBypassIncomingWebhookChannelLock = "bypass_incoming_webhook_channel_lock"
PermissionListPublicTeams = "list_public_teams"
PermissionListPrivateTeams = "list_private_teams"
PermissionJoinPublicTeams = "join_public_teams"
PermissionJoinPrivateTeams = "join_private_teams"
PermissionPermanentDeleteUser = "permanent_delete_user"
PermissionCreateBot = "create_bot"
PermissionReadBots = "read_bots"
PermissionReadOthersBots = "read_others_bots"
PermissionManageBots = "manage_bots"
PermissionManageOthersBots = "manage_others_bots"
PermissionManageSlashCommands = "manage_slash_commands"
PermissionManageOwnSlashCommands = "manage_own_slash_commands"
PermissionDeletePublicChannel = "delete_public_channel"
PermissionDeletePrivateChannel = "delete_private_channel"
PermissionManagePublicChannelProperties = "manage_public_channel_properties"
PermissionManagePrivateChannelProperties = "manage_private_channel_properties"
PermissionConvertPublicChannelToPrivate = "convert_public_channel_to_private"
PermissionConvertPrivateChannelToPublic = "convert_private_channel_to_public"
PermissionViewMembers = "view_members"
PermissionInviteUser = "invite_user"
PermissionInviteGuest = "invite_guest"
PermissionPromoteGuest = "promote_guest"
PermissionDemoteToGuest = "demote_to_guest"
PermissionUseChannelMentions = "use_channel_mentions"
PermissionCreatePost = "create_post"
PermissionCreatePost_PUBLIC = "create_post_public"
PermissionUseGroupMentions = "use_group_mentions"
PermissionAddReaction = "add_reaction"
PermissionRemoveReaction = "remove_reaction"
PermissionManagePublicChannelMembers = "manage_public_channel_members"
PermissionManagePrivateChannelMembers = "manage_private_channel_members"
PermissionReadJobs = "read_jobs"
PermissionManageJobs = "manage_jobs"
PermissionReadOtherUsersTeams = "read_other_users_teams"
PermissionEditOtherUsers = "edit_other_users"
PermissionReadPublicChannelGroups = "read_public_channel_groups"
PermissionReadPrivateChannelGroups = "read_private_channel_groups"
PermissionEditBrand = "edit_brand"
PermissionManageSharedChannels = "manage_shared_channels"
PermissionManageSecureConnections = "manage_secure_connections"
PermissionManageOAuth = "manage_oauth"
PermissionManageRemoteClusters = "manage_remote_clusters" // deprecated; use `manage_secure_connections`
PermissionManageSystem = "manage_system"
PermissionManageTeam = "manage_team"
PermissionManageEmojis = "manage_emojis"
PermissionManageOthersEmojis = "manage_others_emojis"
PermissionCreateEmojis = "create_emojis"
PermissionDeleteEmojis = "delete_emojis"
PermissionDeleteOthersEmojis = "delete_others_emojis"
PermissionManageWebhooks = "manage_webhooks"
PermissionManageOthersWebhooks = "manage_others_webhooks"
PermissionManageIncomingWebhooks = "manage_incoming_webhooks"
PermissionManageOwnIncomingWebhooks = "manage_own_incoming_webhooks"
PermissionManageOthersIncomingWebhooks = "manage_others_incoming_webhooks"
PermissionManageOutgoingWebhooks = "manage_outgoing_webhooks"
PermissionManageOwnOutgoingWebhooks = "manage_own_outgoing_webhooks"
PermissionManageOthersOutgoingWebhooks = "manage_others_outgoing_webhooks"
PermissionBypassIncomingWebhookChannelLock = "bypass_incoming_webhook_channel_lock"
PermissionListPublicTeams = "list_public_teams"
PermissionListPrivateTeams = "list_private_teams"
PermissionJoinPublicTeams = "join_public_teams"
PermissionJoinPrivateTeams = "join_private_teams"
PermissionPermanentDeleteUser = "permanent_delete_user"
PermissionCreateBot = "create_bot"
PermissionReadBots = "read_bots"
PermissionReadOthersBots = "read_others_bots"
PermissionManageBots = "manage_bots"
PermissionManageOthersBots = "manage_others_bots"
PermissionManageSlashCommands = "manage_slash_commands"
PermissionManageOwnSlashCommands = "manage_own_slash_commands"
PermissionDeletePublicChannel = "delete_public_channel"
PermissionDeletePrivateChannel = "delete_private_channel"
PermissionManagePublicChannelProperties = "manage_public_channel_properties"
PermissionManagePrivateChannelProperties = "manage_private_channel_properties"
PermissionManagePublicChannelAutoTranslation = "manage_public_channel_auto_translation"
PermissionManagePrivateChannelAutoTranslation = "manage_private_channel_auto_translation"
PermissionConvertPublicChannelToPrivate = "convert_public_channel_to_private"
PermissionConvertPrivateChannelToPublic = "convert_private_channel_to_public"
PermissionViewMembers = "view_members"
PermissionInviteUser = "invite_user"
PermissionInviteGuest = "invite_guest"
PermissionPromoteGuest = "promote_guest"
PermissionDemoteToGuest = "demote_to_guest"
PermissionUseChannelMentions = "use_channel_mentions"
PermissionCreatePost = "create_post"
PermissionCreatePost_PUBLIC = "create_post_public"
PermissionUseGroupMentions = "use_group_mentions"
PermissionAddReaction = "add_reaction"
PermissionRemoveReaction = "remove_reaction"
PermissionManagePublicChannelMembers = "manage_public_channel_members"
PermissionManagePrivateChannelMembers = "manage_private_channel_members"
PermissionReadJobs = "read_jobs"
PermissionManageJobs = "manage_jobs"
PermissionReadOtherUsersTeams = "read_other_users_teams"
PermissionEditOtherUsers = "edit_other_users"
PermissionReadPublicChannelGroups = "read_public_channel_groups"
PermissionReadPrivateChannelGroups = "read_private_channel_groups"
PermissionEditBrand = "edit_brand"
PermissionManageSharedChannels = "manage_shared_channels"
PermissionManageSecureConnections = "manage_secure_connections"
PermissionManageOAuth = "manage_oauth"
PermissionManageRemoteClusters = "manage_remote_clusters" // deprecated; use `manage_secure_connections`
)
// Deprecated: This function should only be used if a case arises where team and/or channel scheme roles do not need to be migrated.
@@ -1231,6 +1233,22 @@ func (a *App) getAddChannelAccessRulesPermissionMigration() (permissionsMap, err
}, nil
}
func (a *App) getAddChannelAutoTranslationPermissionMigration() (permissionsMap, error) {
return permissionsMap{
permissionTransformation{
On: permissionOr(
isRole(model.ChannelAdminRoleId),
isRole(model.TeamAdminRoleId),
isRole(model.SystemAdminRoleId),
),
Add: []string{
model.PermissionManagePublicChannelAutoTranslation.Id,
model.PermissionManagePrivateChannelAutoTranslation.Id,
},
},
}, nil
}
// Only sysadmins, team admins, and users with channels and groups managements have access to "convert channel to public"
func (a *App) getRestrictAcessToChannelConversionToPublic() (permissionsMap, error) {
return []permissionTransformation{
@@ -1304,6 +1322,7 @@ func (s *Server) doPermissionsMigrations() error {
{Key: model.MigrationAddSysconsoleMobileSecurityPermission, Migration: a.addSysConsoleMobileSecurityPermission},
{Key: model.MigrationKeyAddChannelBannerPermissions, Migration: a.getAddChannelBannerPermissionMigration},
{Key: model.MigrationKeyAddChannelAccessRulesPermission, Migration: a.getAddChannelAccessRulesPermissionMigration},
{Key: model.MigrationKeyAddChannelAutoTranslationPermissions, Migration: a.getAddChannelAutoTranslationPermissionMigration},
}
roles, err := s.Store().Role().GetAll()
@@ -7,8 +7,6 @@ import (
"bytes"
"fmt"
"github.com/pkg/errors"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/v8/channels/store"
)
@@ -45,31 +43,6 @@ func (s LocalCacheAutoTranslationStore) ClearCaches() {
}
}
// IsChannelEnabled checks if auto-translation is enabled for a channel
// Uses the existing Channel cache instead of maintaining a separate cache
func (s LocalCacheAutoTranslationStore) IsChannelEnabled(channelID string) (bool, error) {
// Get channel from cache (with DB fallback)
channel, err := s.rootStore.Channel().Get(channelID, true)
if err != nil {
return false, errors.Wrapf(err, "failed to get channel for auto-translation check, channel_id=%s", channelID)
}
return channel.AutoTranslation, nil
}
// SetChannelEnabled sets auto-translation status for a channel and invalidates Channel cache
func (s LocalCacheAutoTranslationStore) SetChannelEnabled(channelID string, enabled bool) error {
err := s.AutoTranslationStore.SetChannelEnabled(channelID, enabled)
if err != nil {
return err
}
// Invalidate the Channel cache since we modified channel.autotranslation
s.rootStore.Channel().InvalidateChannel(channelID)
return nil
}
// IsUserEnabled checks if auto-translation is enabled for a user in a channel (with caching)
func (s LocalCacheAutoTranslationStore) IsUserEnabled(userID, channelID string) (bool, error) {
key := userAutoTranslationKey(userID, channelID)
@@ -1031,27 +1031,6 @@ func (s *RetryLayerAutoTranslationStore) InvalidateUserLocaleCache(userID string
}
func (s *RetryLayerAutoTranslationStore) IsChannelEnabled(channelID string) (bool, error) {
tries := 0
for {
result, err := s.AutoTranslationStore.IsChannelEnabled(channelID)
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 *RetryLayerAutoTranslationStore) IsUserEnabled(userID string, channelID string) (bool, error) {
tries := 0
@@ -1094,27 +1073,6 @@ func (s *RetryLayerAutoTranslationStore) Save(translation *model.Translation) er
}
func (s *RetryLayerAutoTranslationStore) SetChannelEnabled(channelID string, enabled bool) error {
tries := 0
for {
err := s.AutoTranslationStore.SetChannelEnabled(channelID, enabled)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerBotStore) Get(userID string, includeDeleted bool) (*model.Bot, error) {
tries := 0
@@ -51,61 +51,13 @@ func newSqlAutoTranslationStore(sqlStore *SqlStore) store.AutoTranslationStore {
}
}
// IsChannelEnabled checks if auto-translation is enabled for a channel
// Uses the existing Channel cache instead of maintaining a separate cache
// Thus this method is really for completeness; callers should use the Channel cache
func (s *SqlAutoTranslationStore) IsChannelEnabled(channelID string) (bool, error) {
query := s.getQueryBuilder().
Select("AutoTranslation").
From("Channels").
Where(sq.Eq{"Id": channelID})
queryString, args, err := query.ToSql()
if err != nil {
return false, errors.Wrap(err, "failed to build query for IsChannelEnabled")
}
var enabled bool
if err := s.GetReplica().Get(&enabled, queryString, args...); err != nil {
if err == sql.ErrNoRows {
return false, store.NewErrNotFound("Channel", channelID)
}
return false, errors.Wrapf(err, "failed to get channel enabled status for channel_id=%s", channelID)
}
return enabled, nil
}
func (s *SqlAutoTranslationStore) SetChannelEnabled(channelID string, enabled bool) error {
query := s.getQueryBuilder().
Update("Channels").
Set("AutoTranslation", enabled).
Set("UpdateAt", model.GetMillis()).
Where(sq.Eq{"Id": channelID})
result, err := s.GetMaster().ExecBuilder(query)
if err != nil {
return errors.Wrapf(err, "failed to set channel enabled for channel_id=%s", channelID)
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return errors.Wrap(err, "failed to get rows affected for SetChannelEnabled")
}
if rowsAffected == 0 {
return store.NewErrNotFound("Channel", channelID)
}
return nil
}
func (s *SqlAutoTranslationStore) IsUserEnabled(userID, channelID string) (bool, error) {
query := s.getQueryBuilder().
Select("cm.AutoTranslationDisabled").
From("ChannelMembers cm").
Join("Channels c ON cm.Channelid = c.id").
Join("Channels c ON cm.ChannelId = c.Id").
Where(sq.Eq{"cm.UserId": userID, "cm.ChannelId": channelID}).
Where("cm.AutoTranslationDisabled != true").
Where("c.AutoTranslation = true")
var disabled bool
@@ -127,7 +79,7 @@ func (s *SqlAutoTranslationStore) GetUserLanguage(userID, channelID string) (str
Join("Channels c ON cm.ChannelId = c.Id").
Where(sq.Eq{"u.Id": userID, "c.Id": channelID}).
Where("c.AutoTranslation = true").
Where("cm.AutoTranslationDisabled = false")
Where("cm.AutoTranslationDisabled != true")
var locale string
if err := s.GetReplica().GetBuilder(&locale, query); err != nil {
@@ -148,7 +100,7 @@ func (s *SqlAutoTranslationStore) GetActiveDestinationLanguages(channelID, exclu
Join("Users u ON u.Id = cm.UserId").
Where(sq.Eq{"cm.ChannelId": channelID}).
Where("c.AutoTranslation = true").
Where("cm.AutoTranslationDisabled = false")
Where("cm.AutoTranslationDisabled != true")
// Filter to specific user IDs if provided (e.g., users with active WebSocket connections)
// When filterUserIDs is non-nil and non-empty, squirrel converts it to an IN clause
@@ -813,7 +813,8 @@ func (s SqlChannelStore) updateChannelT(transaction *sqlxTxWrapper, channel *mod
TotalMsgCountRoot=:TotalMsgCountRoot,
LastRootPostAt=:LastRootPostAt,
BannerInfo=:BannerInfo,
DefaultCategoryName=:DefaultCategoryName
DefaultCategoryName=:DefaultCategoryName,
AutoTranslation=:AutoTranslation
WHERE Id=:Id`, channel)
if err != nil {
if IsUniqueConstraintError(err, []string{"Name", "channels_name_teamid_key"}) {
-2
View File
@@ -1158,8 +1158,6 @@ type AttributesStore interface {
}
type AutoTranslationStore interface {
IsChannelEnabled(channelID string) (bool, error)
SetChannelEnabled(channelID string, enabled bool) error
IsUserEnabled(userID, channelID string) (bool, error)
GetUserLanguage(userID, channelID string) (string, error)
// GetActiveDestinationLanguages returns distinct locales of users who have auto-translation enabled.
@@ -15,142 +15,11 @@ import (
)
func TestAutoTranslationStore(t *testing.T, rctx request.CTX, ss store.Store, s SqlStore) {
t.Run("IsChannelEnabled", func(t *testing.T) { testAutoTranslationIsChannelEnabled(t, rctx, ss) })
t.Run("SetChannelEnabled", func(t *testing.T) { testAutoTranslationSetChannelEnabled(t, rctx, ss) })
t.Run("IsUserEnabled", func(t *testing.T) { testAutoTranslationIsUserEnabled(t, rctx, ss) })
t.Run("GetUserLanguage", func(t *testing.T) { testAutoTranslationGetUserLanguage(t, rctx, ss) })
t.Run("GetActiveDestinationLanguages", func(t *testing.T) { testAutoTranslationGetActiveDestinationLanguages(t, rctx, ss) })
}
func testAutoTranslationIsChannelEnabled(t *testing.T, rctx request.CTX, ss store.Store) {
// Setup: Create a test team and channel
team := &model.Team{
DisplayName: "Test Team",
Name: "test-team-" + model.NewId(),
Email: "test@example.com",
Type: model.TeamOpen,
}
team, err := ss.Team().Save(team)
require.NoError(t, err)
channel := &model.Channel{
TeamId: team.Id,
DisplayName: "Test Channel",
Name: "test-channel-" + model.NewId(),
Type: model.ChannelTypeOpen,
}
channel, nErr := ss.Channel().Save(rctx, channel, 999)
require.NoError(t, nErr)
defer func() {
_ = ss.Team().PermanentDelete(team.Id)
_ = ss.Channel().PermanentDelete(rctx, channel.Id)
}()
t.Run("default value is false", func(t *testing.T) {
enabled, appErr := ss.AutoTranslation().IsChannelEnabled(channel.Id)
require.NoError(t, appErr)
assert.False(t, enabled, "autotranslation should be disabled by default")
})
t.Run("returns true after enabling", func(t *testing.T) {
// Enable autotranslation
appErr := ss.AutoTranslation().SetChannelEnabled(channel.Id, true)
require.NoError(t, appErr)
// Verify it's enabled
enabled, appErr := ss.AutoTranslation().IsChannelEnabled(channel.Id)
require.NoError(t, appErr)
assert.True(t, enabled)
})
t.Run("returns false after disabling", func(t *testing.T) {
// Disable autotranslation
appErr := ss.AutoTranslation().SetChannelEnabled(channel.Id, false)
require.NoError(t, appErr)
// Verify it's disabled
enabled, appErr := ss.AutoTranslation().IsChannelEnabled(channel.Id)
require.NoError(t, appErr)
assert.False(t, enabled)
})
t.Run("returns error for non-existent channel", func(t *testing.T) {
enabled, err := ss.AutoTranslation().IsChannelEnabled("nonexistent")
assert.Error(t, err)
assert.True(t, store.IsErrNotFound(err))
assert.False(t, enabled)
})
}
func testAutoTranslationSetChannelEnabled(t *testing.T, rctx request.CTX, ss store.Store) {
// Setup: Create a test team and channel
team := &model.Team{
DisplayName: "Test Team",
Name: "test-team-" + model.NewId(),
Email: "test@example.com",
Type: model.TeamOpen,
}
team, err := ss.Team().Save(team)
require.NoError(t, err)
channel := &model.Channel{
TeamId: team.Id,
DisplayName: "Test Channel",
Name: "test-channel-" + model.NewId(),
Type: model.ChannelTypeOpen,
}
channel, nErr := ss.Channel().Save(rctx, channel, 999)
require.NoError(t, nErr)
defer func() {
_ = ss.Team().PermanentDelete(team.Id)
_ = ss.Channel().PermanentDelete(rctx, channel.Id)
}()
t.Run("successfully enables autotranslation", func(t *testing.T) {
appErr := ss.AutoTranslation().SetChannelEnabled(channel.Id, true)
require.NoError(t, appErr)
// Verify via IsChannelEnabled
enabled, appErr := ss.AutoTranslation().IsChannelEnabled(channel.Id)
require.NoError(t, appErr)
assert.True(t, enabled)
})
t.Run("successfully disables autotranslation", func(t *testing.T) {
appErr := ss.AutoTranslation().SetChannelEnabled(channel.Id, false)
require.NoError(t, appErr)
// Verify via IsChannelEnabled
enabled, appErr := ss.AutoTranslation().IsChannelEnabled(channel.Id)
require.NoError(t, appErr)
assert.False(t, enabled)
})
t.Run("updates channel timestamp", func(t *testing.T) {
// Get original update timestamp
originalChannel, nErr := ss.Channel().Get(channel.Id, true)
require.NoError(t, nErr)
originalUpdateAt := originalChannel.UpdateAt
// Enable autotranslation
appErr := ss.AutoTranslation().SetChannelEnabled(channel.Id, true)
require.NoError(t, appErr)
// Verify timestamp was updated
updatedChannel, nErr := ss.Channel().Get(channel.Id, true)
require.NoError(t, nErr)
assert.Greater(t, updatedChannel.UpdateAt, originalUpdateAt)
})
t.Run("returns error for non-existent channel", func(t *testing.T) {
err := ss.AutoTranslation().SetChannelEnabled("nonexistent", true)
assert.Error(t, err)
assert.True(t, store.IsErrNotFound(err))
})
}
func testAutoTranslationIsUserEnabled(t *testing.T, rctx request.CTX, ss store.Store) {
// Setup: Create team, channel, and user
team := &model.Team{
@@ -196,61 +65,64 @@ func testAutoTranslationIsUserEnabled(t *testing.T, rctx request.CTX, ss store.S
t.Run("returns false when channel is disabled", func(t *testing.T) {
// Channel autotranslation is disabled by default
enabled, appErr := ss.AutoTranslation().IsUserEnabled(user.Id, channel.Id)
require.NoError(t, appErr)
enabled, err := ss.AutoTranslation().IsUserEnabled(user.Id, channel.Id)
require.NoError(t, err)
assert.False(t, enabled)
})
t.Run("returns false when channel enabled but user disabled", func(t *testing.T) {
// Enable channel autotranslation
appErr := ss.AutoTranslation().SetChannelEnabled(channel.Id, true)
require.NoError(t, appErr)
channel.AutoTranslation = true
channel, nErr = ss.Channel().Update(rctx, channel)
require.NoError(t, nErr)
// Disable user autotranslation (AutoTranslationDisabled = true means disabled)
member.AutoTranslationDisabled = true
_, appErr = ss.Channel().UpdateMember(rctx, member)
require.NoError(t, appErr)
_, nErr = ss.Channel().UpdateMember(rctx, member)
require.NoError(t, nErr)
enabled, appErr := ss.AutoTranslation().IsUserEnabled(user.Id, channel.Id)
require.NoError(t, appErr)
enabled, getUserEnabledErr := ss.AutoTranslation().IsUserEnabled(user.Id, channel.Id)
require.NoError(t, getUserEnabledErr)
assert.False(t, enabled)
})
t.Run("returns true when both channel and user enabled", func(t *testing.T) {
// Enable channel autotranslation
appErr := ss.AutoTranslation().SetChannelEnabled(channel.Id, true)
require.NoError(t, appErr)
channel.AutoTranslation = true
channel, nErr = ss.Channel().Update(rctx, channel)
require.NoError(t, nErr)
// Enable user autotranslation
member.AutoTranslationDisabled = false
_, appErr = ss.Channel().UpdateMember(rctx, member)
require.NoError(t, appErr)
_, nErr = ss.Channel().UpdateMember(rctx, member)
require.NoError(t, nErr)
// Verify both are enabled
enabled, appErr := ss.AutoTranslation().IsUserEnabled(user.Id, channel.Id)
require.NoError(t, appErr)
enabled, getUserEnabledErr := ss.AutoTranslation().IsUserEnabled(user.Id, channel.Id)
require.NoError(t, getUserEnabledErr)
assert.True(t, enabled)
})
t.Run("returns false after disabling user", func(t *testing.T) {
// Ensure channel is enabled
appErr := ss.AutoTranslation().SetChannelEnabled(channel.Id, true)
require.NoError(t, appErr)
channel.AutoTranslation = true
channel, nErr = ss.Channel().Update(rctx, channel)
require.NoError(t, nErr)
// Disable user autotranslation
member.AutoTranslationDisabled = true
_, appErr = ss.Channel().UpdateMember(rctx, member)
require.NoError(t, appErr)
_, nErr = ss.Channel().UpdateMember(rctx, member)
require.NoError(t, nErr)
// Verify user is disabled
enabled, appErr := ss.AutoTranslation().IsUserEnabled(user.Id, channel.Id)
require.NoError(t, appErr)
enabled, nErr := ss.AutoTranslation().IsUserEnabled(user.Id, channel.Id)
require.NoError(t, nErr)
assert.False(t, enabled)
})
t.Run("returns false for non-existent user or channel", func(t *testing.T) {
enabled, appErr := ss.AutoTranslation().IsUserEnabled("nonexistent", channel.Id)
require.NoError(t, appErr)
enabled, nErr := ss.AutoTranslation().IsUserEnabled("nonexistent", channel.Id)
require.NoError(t, nErr)
assert.False(t, enabled)
})
}
@@ -318,56 +190,59 @@ func testAutoTranslationGetUserLanguage(t *testing.T, rctx request.CTX, ss store
})
t.Run("returns empty when channel enabled but user disabled", func(t *testing.T) {
appErr := ss.AutoTranslation().SetChannelEnabled(channel.Id, true)
require.NoError(t, appErr)
channel.AutoTranslation = true
channel, nErr = ss.Channel().Update(rctx, channel)
require.NoError(t, nErr)
// Disable user autotranslation (AutoTranslationDisabled = true means disabled)
members[userEN.Id].AutoTranslationDisabled = true
_, appErr = ss.Channel().UpdateMember(rctx, members[userEN.Id])
require.NoError(t, appErr)
_, nErr = ss.Channel().UpdateMember(rctx, members[userEN.Id])
require.NoError(t, nErr)
locale, appErr := ss.AutoTranslation().GetUserLanguage(userEN.Id, channel.Id)
require.NoError(t, appErr)
locale, getLocaleErr := ss.AutoTranslation().GetUserLanguage(userEN.Id, channel.Id)
require.NoError(t, getLocaleErr)
assert.Empty(t, locale)
})
t.Run("returns user locale when both enabled", func(t *testing.T) {
// Enable channel
appErr := ss.AutoTranslation().SetChannelEnabled(channel.Id, true)
require.NoError(t, appErr)
channel.AutoTranslation = true
channel, nErr = ss.Channel().Update(rctx, channel)
require.NoError(t, nErr)
// Enable user (set AutoTranslationDisabled = false)
members[userEN.Id].AutoTranslationDisabled = false
_, appErr = ss.Channel().UpdateMember(rctx, members[userEN.Id])
require.NoError(t, appErr)
_, nErr = ss.Channel().UpdateMember(rctx, members[userEN.Id])
require.NoError(t, nErr)
// Get language
locale, appErr := ss.AutoTranslation().GetUserLanguage(userEN.Id, channel.Id)
require.NoError(t, appErr)
locale, getLocaleErr := ss.AutoTranslation().GetUserLanguage(userEN.Id, channel.Id)
require.NoError(t, getLocaleErr)
assert.Equal(t, "en", locale)
})
t.Run("returns correct locale for different users", func(t *testing.T) {
// Enable channel
appErr := ss.AutoTranslation().SetChannelEnabled(channel.Id, true)
require.NoError(t, appErr)
channel.AutoTranslation = true
channel, nErr = ss.Channel().Update(rctx, channel)
require.NoError(t, nErr)
// Enable both users (set AutoTranslationDisabled = false)
members[userEN.Id].AutoTranslationDisabled = false
_, appErr = ss.Channel().UpdateMember(rctx, members[userEN.Id])
require.NoError(t, appErr)
_, nErr = ss.Channel().UpdateMember(rctx, members[userEN.Id])
require.NoError(t, nErr)
members[userES.Id].AutoTranslationDisabled = false
_, appErr = ss.Channel().UpdateMember(rctx, members[userES.Id])
require.NoError(t, appErr)
_, nErr = ss.Channel().UpdateMember(rctx, members[userES.Id])
require.NoError(t, nErr)
// Verify English user
locale, appErr := ss.AutoTranslation().GetUserLanguage(userEN.Id, channel.Id)
require.NoError(t, appErr)
locale, nErr := ss.AutoTranslation().GetUserLanguage(userEN.Id, channel.Id)
require.NoError(t, nErr)
assert.Equal(t, "en", locale)
// Verify Spanish user
locale, appErr = ss.AutoTranslation().GetUserLanguage(userES.Id, channel.Id)
require.NoError(t, appErr)
locale, nErr = ss.AutoTranslation().GetUserLanguage(userES.Id, channel.Id)
require.NoError(t, nErr)
assert.Equal(t, "es", locale)
})
}
@@ -435,8 +310,9 @@ func testAutoTranslationGetActiveDestinationLanguages(t *testing.T, rctx request
t.Run("returns all enabled user languages", func(t *testing.T) {
// Enable channel
appErr := ss.AutoTranslation().SetChannelEnabled(channel.Id, true)
require.NoError(t, appErr)
channel.AutoTranslation = true
channel, nErr = ss.Channel().Update(rctx, channel)
require.NoError(t, nErr)
languages, appErr := ss.AutoTranslation().GetActiveDestinationLanguages(channel.Id, "", nil)
require.NoError(t, appErr)
@@ -451,8 +327,9 @@ func testAutoTranslationGetActiveDestinationLanguages(t *testing.T, rctx request
t.Run("excludes specified user", func(t *testing.T) {
// Enable channel
appErr := ss.AutoTranslation().SetChannelEnabled(channel.Id, true)
require.NoError(t, appErr)
channel.AutoTranslation = true
channel, nErr = ss.Channel().Update(rctx, channel)
require.NoError(t, nErr)
// Exclude Spanish user
languages, appErr := ss.AutoTranslation().GetActiveDestinationLanguages(channel.Id, users[1].Id, nil)
@@ -468,8 +345,9 @@ func testAutoTranslationGetActiveDestinationLanguages(t *testing.T, rctx request
t.Run("filters to specific users", func(t *testing.T) {
// Enable channel
appErr := ss.AutoTranslation().SetChannelEnabled(channel.Id, true)
require.NoError(t, appErr)
channel.AutoTranslation = true
channel, nErr = ss.Channel().Update(rctx, channel)
require.NoError(t, nErr)
// Filter to only first two users (en, es)
filterIDs := []string{users[0].Id, users[1].Id}
@@ -484,8 +362,9 @@ func testAutoTranslationGetActiveDestinationLanguages(t *testing.T, rctx request
t.Run("filters and excludes user", func(t *testing.T) {
// Enable channel
appErr := ss.AutoTranslation().SetChannelEnabled(channel.Id, true)
require.NoError(t, appErr)
channel.AutoTranslation = true
channel, nErr = ss.Channel().Update(rctx, channel)
require.NoError(t, nErr)
// Filter to first two users but exclude the first one
filterIDs := []string{users[0].Id, users[1].Id}
@@ -237,34 +237,6 @@ func (_m *AutoTranslationStore) InvalidateUserLocaleCache(userID string) {
_m.Called(userID)
}
// IsChannelEnabled provides a mock function with given fields: channelID
func (_m *AutoTranslationStore) IsChannelEnabled(channelID string) (bool, error) {
ret := _m.Called(channelID)
if len(ret) == 0 {
panic("no return value specified for IsChannelEnabled")
}
var r0 bool
var r1 error
if rf, ok := ret.Get(0).(func(string) (bool, error)); ok {
return rf(channelID)
}
if rf, ok := ret.Get(0).(func(string) bool); ok {
r0 = rf(channelID)
} else {
r0 = ret.Get(0).(bool)
}
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(channelID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// IsUserEnabled provides a mock function with given fields: userID, channelID
func (_m *AutoTranslationStore) IsUserEnabled(userID string, channelID string) (bool, error) {
ret := _m.Called(userID, channelID)
@@ -311,24 +283,6 @@ func (_m *AutoTranslationStore) Save(translation *model.Translation) error {
return r0
}
// SetChannelEnabled provides a mock function with given fields: channelID, enabled
func (_m *AutoTranslationStore) SetChannelEnabled(channelID string, enabled bool) error {
ret := _m.Called(channelID, enabled)
if len(ret) == 0 {
panic("no return value specified for SetChannelEnabled")
}
var r0 error
if rf, ok := ret.Get(0).(func(string, bool) error); ok {
r0 = rf(channelID, enabled)
} else {
r0 = ret.Error(0)
}
return r0
}
// SetUserEnabled provides a mock function with given fields: userID, channelID, enabled
func (_m *AutoTranslationStore) SetUserEnabled(userID string, channelID string, enabled bool) error {
ret := _m.Called(userID, channelID, enabled)
@@ -946,22 +946,6 @@ func (s *TimerLayerAutoTranslationStore) InvalidateUserLocaleCache(userID string
}
}
func (s *TimerLayerAutoTranslationStore) IsChannelEnabled(channelID string) (bool, error) {
start := time.Now()
result, err := s.AutoTranslationStore.IsChannelEnabled(channelID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("AutoTranslationStore.IsChannelEnabled", success, elapsed)
}
return result, err
}
func (s *TimerLayerAutoTranslationStore) IsUserEnabled(userID string, channelID string) (bool, error) {
start := time.Now()
@@ -994,22 +978,6 @@ func (s *TimerLayerAutoTranslationStore) Save(translation *model.Translation) er
return err
}
func (s *TimerLayerAutoTranslationStore) SetChannelEnabled(channelID string, enabled bool) error {
start := time.Now()
err := s.AutoTranslationStore.SetChannelEnabled(channelID, enabled)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("AutoTranslationStore.SetChannelEnabled", success, elapsed)
}
return err
}
func (s *TimerLayerBotStore) Get(userID string, includeDeleted bool) (*model.Bot, error) {
start := time.Now()
+1
View File
@@ -90,6 +90,7 @@ func GetMockStoreForSetupFunctions() *mocks.Store {
systemStore.On("GetByName", model.MigrationAddSysconsoleMobileSecurityPermission).Return(&model.System{Name: model.MigrationAddSysconsoleMobileSecurityPermission, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyAddChannelBannerPermissions).Return(&model.System{Name: model.MigrationKeyAddChannelBannerPermissions, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyAddChannelAccessRulesPermission).Return(&model.System{Name: model.MigrationKeyAddChannelAccessRulesPermission, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyAddChannelAutoTranslationPermissions).Return(&model.System{Name: model.MigrationKeyAddChannelAutoTranslationPermissions, Value: "true"}, nil)
systemStore.On("InsertIfExists", mock.AnythingOfType("*model.System")).Return(&model.System{}, nil).Once()
systemStore.On("Save", mock.AnythingOfType("*model.System")).Return(nil)
@@ -249,6 +249,8 @@ func (s *MmctlUnitTestSuite) TestResetPermissionsCmd() {
"manage_public_channel_banner",
"manage_private_channel_banner",
"manage_channel_access_rules",
"manage_public_channel_auto_translation",
"manage_private_channel_auto_translation",
}
expectedPatch := &model.RolePatch{
Permissions: &expectedPermissions,
+1
View File
@@ -249,6 +249,7 @@ func GenerateClientConfig(c *model.Config, telemetryID string, license *model.Li
props["ContentFlaggingEnabled"] = strconv.FormatBool(c.FeatureFlags.ContentFlagging && *c.ContentFlaggingSettings.EnableContentFlagging)
props["EnableAutoTranslation"] = strconv.FormatBool(c.FeatureFlags.AutoTranslation && *c.AutoTranslationSettings.Enable)
props["RestrictDMAndGMAutotranslation"] = strconv.FormatBool(*c.AutoTranslationSettings.RestrictDMAndGM)
}
}
-4
View File
@@ -23,10 +23,6 @@ type AutoTranslationInterface interface {
// Returns false if the feature is unavailable (license, config, etc.).
IsChannelEnabled(channelID string) (bool, *model.AppError)
// SetChannelEnabled enables or disables auto-translation for a channel.
// Only available when the feature is properly licensed and configured.
SetChannelEnabled(channelID string, enabled bool) *model.AppError
// IsUserEnabled checks if auto-translation is enabled for a specific user in a channel.
// This checks both channel enablement AND user opt-in status.
// Returns false if the feature is unavailable or the user hasn't opted in.
@@ -256,26 +256,6 @@ func (_m *AutoTranslationInterface) MakeWorker() model.Worker {
return r0
}
// SetChannelEnabled provides a mock function with given fields: channelID, enabled
func (_m *AutoTranslationInterface) SetChannelEnabled(channelID string, enabled bool) *model.AppError {
ret := _m.Called(channelID, enabled)
if len(ret) == 0 {
panic("no return value specified for SetChannelEnabled")
}
var r0 *model.AppError
if rf, ok := ret.Get(0).(func(string, bool) *model.AppError); ok {
r0 = rf(channelID, enabled)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AppError)
}
}
return r0
}
// SetUserEnabled provides a mock function with given fields: channelID, userID, enabled
func (_m *AutoTranslationInterface) SetUserEnabled(channelID string, userID string, enabled bool) *model.AppError {
ret := _m.Called(channelID, userID, enabled)
+28
View File
@@ -455,10 +455,22 @@
"id": "api.channel.patch_channel_moderations_for_channel.restricted_permission.app_error",
"translation": "Cannot add a permission that is restricted by the team or system permission scheme."
},
{
"id": "api.channel.patch_update_channel.auto_translation_restricted.app_error",
"translation": "Auto translation is not allowed for this channel."
},
{
"id": "api.channel.patch_update_channel.feature_not_available.app_error",
"translation": "Auto translation feature is not available."
},
{
"id": "api.channel.patch_update_channel.forbidden.app_error",
"translation": "Failed to update the channel."
},
{
"id": "api.channel.patch_update_channel.no_changes.app_error",
"translation": "No changes in the patch."
},
{
"id": "api.channel.patch_update_channel.restricted_dm.app_error",
"translation": "Cannot update a restricted direct message channel."
@@ -471,6 +483,22 @@
"id": "api.channel.post_channel_privacy_message.error",
"translation": "Failed to post channel privacy update message."
},
{
"id": "api.channel.post_update_channel_autotranslation_message.create_post.error",
"translation": "Failed to post Auto-translation update message."
},
{
"id": "api.channel.post_update_channel_autotranslation_message.disabled",
"translation": "@%s disabled Auto-translation for this channel. All new messages will appear in the original language."
},
{
"id": "api.channel.post_update_channel_autotranslation_message.enabled",
"translation": "@%s enabled Auto-translation for this channel. All new messages will appear in your preferred language."
},
{
"id": "api.channel.post_update_channel_autotranslation_message.retrieve_user.error",
"translation": "Failed to retrieve user while updating Auto-translation status"
},
{
"id": "api.channel.post_update_channel_displayname_message_and_forget.create_post.error",
"translation": "Failed to post displayname update message"
+5
View File
@@ -152,6 +152,7 @@ type ChannelPatch struct {
Purpose *string `json:"purpose"`
GroupConstrained *bool `json:"group_constrained"`
BannerInfo *ChannelBannerInfo `json:"banner_info"`
AutoTranslation *bool `json:"autotranslation"`
}
func (c *ChannelPatch) Auditable() map[string]any {
@@ -399,6 +400,10 @@ func (o *Channel) Patch(patch *ChannelPatch) {
o.BannerInfo.BackgroundColor = patch.BannerInfo.BackgroundColor
}
}
if patch.AutoTranslation != nil {
o.AutoTranslation = *patch.AutoTranslation
}
}
func (o *Channel) MakeNonNil() {
+5
View File
@@ -2785,6 +2785,7 @@ func (s *LocalizationSettings) SetDefaults() {
type AutoTranslationSettings struct {
Enable *bool `access:"site_localization,cloud_restrictable"`
RestrictDMAndGM *bool `access:"site_localization,cloud_restrictable"`
Provider *string `access:"site_localization,cloud_restrictable"`
TargetLanguages *[]string `access:"site_localization,cloud_restrictable"`
Workers *int `access:"site_localization,cloud_restrictable"`
@@ -2833,6 +2834,10 @@ func (s *AutoTranslationSettings) SetDefaults() {
s.Agents = &AgentsProviderSettings{}
}
s.Agents.SetDefaults()
if s.RestrictDMAndGM == nil {
s.RestrictDMAndGM = NewPointer(false)
}
}
func (s *LibreTranslateProviderSettings) SetDefaults() {
+1
View File
@@ -57,4 +57,5 @@ const (
MigrationAddSysconsoleMobileSecurityPermission = "add_sysconsole_mobile_security_permission"
MigrationKeyAddChannelBannerPermissions = "add_channel_banner_permissions"
MigrationKeyAddChannelAccessRulesPermission = "add_channel_access_rules_permission"
MigrationKeyAddChannelAutoTranslationPermissions = "add_channel_auto_translation_permissions"
)
+16
View File
@@ -46,6 +46,8 @@ var PermissionCreateDirectChannel *Permission
var PermissionCreateGroupChannel *Permission
var PermissionManagePublicChannelProperties *Permission
var PermissionManagePrivateChannelProperties *Permission
var PermissionManagePublicChannelAutoTranslation *Permission
var PermissionManagePrivateChannelAutoTranslation *Permission
var PermissionListPublicTeams *Permission
var PermissionJoinPublicTeams *Permission
var PermissionListPrivateTeams *Permission
@@ -544,6 +546,18 @@ func initializePermissions() {
"authentication.permissions.manage_private_channel_properties.description",
PermissionScopeChannel,
}
PermissionManagePublicChannelAutoTranslation = &Permission{
"manage_public_channel_auto_translation",
"authentication.permissions.manage_public_channel_auto_translation.name",
"authentication.permissions.manage_public_channel_auto_translation.description",
PermissionScopeChannel,
}
PermissionManagePrivateChannelAutoTranslation = &Permission{
"manage_private_channel_auto_translation",
"authentication.permissions.manage_private_channel_auto_translation.name",
"authentication.permissions.manage_private_channel_auto_translation.description",
PermissionScopeChannel,
}
PermissionListPublicTeams = &Permission{
"list_public_teams",
"authentication.permissions.list_public_teams.name",
@@ -2547,6 +2561,8 @@ func initializePermissions() {
PermissionManageChannelRoles,
PermissionManagePublicChannelProperties,
PermissionManagePrivateChannelProperties,
PermissionManagePublicChannelAutoTranslation,
PermissionManagePrivateChannelAutoTranslation,
PermissionConvertPublicChannelToPrivate,
PermissionConvertPrivateChannelToPublic,
PermissionDeletePublicChannel,
+35 -33
View File
@@ -26,39 +26,40 @@ import (
type PostContextKey string
const (
PostSystemMessagePrefix = "system_"
PostTypeDefault = ""
PostTypeSlackAttachment = "slack_attachment"
PostTypeSystemGeneric = "system_generic"
PostTypeJoinLeave = "system_join_leave" // Deprecated, use PostJoinChannel or PostLeaveChannel instead
PostTypeJoinChannel = "system_join_channel"
PostTypeGuestJoinChannel = "system_guest_join_channel"
PostTypeLeaveChannel = "system_leave_channel"
PostTypeJoinTeam = "system_join_team"
PostTypeLeaveTeam = "system_leave_team"
PostTypeAutoResponder = "system_auto_responder"
PostTypeAddRemove = "system_add_remove" // Deprecated, use PostAddToChannel or PostRemoveFromChannel instead
PostTypeAddToChannel = "system_add_to_channel"
PostTypeAddGuestToChannel = "system_add_guest_to_chan"
PostTypeRemoveFromChannel = "system_remove_from_channel"
PostTypeMoveChannel = "system_move_channel"
PostTypeAddToTeam = "system_add_to_team"
PostTypeRemoveFromTeam = "system_remove_from_team"
PostTypeHeaderChange = "system_header_change"
PostTypeDisplaynameChange = "system_displayname_change"
PostTypeConvertChannel = "system_convert_channel"
PostTypePurposeChange = "system_purpose_change"
PostTypeChannelDeleted = "system_channel_deleted"
PostTypeChannelRestored = "system_channel_restored"
PostTypeEphemeral = "system_ephemeral"
PostTypeChangeChannelPrivacy = "system_change_chan_privacy"
PostTypeWrangler = "system_wrangler"
PostTypeGMConvertedToChannel = "system_gm_to_channel"
PostTypeAddBotTeamsChannels = "add_bot_teams_channels"
PostTypeMe = "me"
PostCustomTypePrefix = "custom_"
PostTypeReminder = "reminder"
PostTypeBurnOnRead = "burn_on_read"
PostSystemMessagePrefix = "system_"
PostTypeDefault = ""
PostTypeSlackAttachment = "slack_attachment"
PostTypeSystemGeneric = "system_generic"
PostTypeJoinLeave = "system_join_leave" // Deprecated, use PostJoinChannel or PostLeaveChannel instead
PostTypeJoinChannel = "system_join_channel"
PostTypeGuestJoinChannel = "system_guest_join_channel"
PostTypeLeaveChannel = "system_leave_channel"
PostTypeJoinTeam = "system_join_team"
PostTypeLeaveTeam = "system_leave_team"
PostTypeAutoResponder = "system_auto_responder"
PostTypeAutotranslationChange = "system_autotranslation"
PostTypeAddRemove = "system_add_remove" // Deprecated, use PostAddToChannel or PostRemoveFromChannel instead
PostTypeAddToChannel = "system_add_to_channel"
PostTypeAddGuestToChannel = "system_add_guest_to_chan"
PostTypeRemoveFromChannel = "system_remove_from_channel"
PostTypeMoveChannel = "system_move_channel"
PostTypeAddToTeam = "system_add_to_team"
PostTypeRemoveFromTeam = "system_remove_from_team"
PostTypeHeaderChange = "system_header_change"
PostTypeDisplaynameChange = "system_displayname_change"
PostTypeConvertChannel = "system_convert_channel"
PostTypePurposeChange = "system_purpose_change"
PostTypeChannelDeleted = "system_channel_deleted"
PostTypeChannelRestored = "system_channel_restored"
PostTypeEphemeral = "system_ephemeral"
PostTypeChangeChannelPrivacy = "system_change_chan_privacy"
PostTypeWrangler = "system_wrangler"
PostTypeGMConvertedToChannel = "system_gm_to_channel"
PostTypeAddBotTeamsChannels = "add_bot_teams_channels"
PostTypeMe = "me"
PostCustomTypePrefix = "custom_"
PostTypeReminder = "reminder"
PostTypeBurnOnRead = "burn_on_read"
PostFileidsMaxRunes = 300
PostFilenamesMaxRunes = 4000
@@ -523,6 +524,7 @@ func (o *Post) IsValid(maxPostSize int) *AppError {
PostTypeMe,
PostTypeWrangler,
PostTypeGMConvertedToChannel,
PostTypeAutotranslationChange,
PostTypeBurnOnRead:
default:
if !strings.HasPrefix(o.Type, PostCustomTypePrefix) {
+4
View File
@@ -119,6 +119,8 @@ func init() {
PermissionSysconsoleWriteUserManagementChannels.Id: {
PermissionManagePublicChannelProperties,
PermissionManagePrivateChannelProperties,
PermissionManagePublicChannelAutoTranslation,
PermissionManagePrivateChannelAutoTranslation,
PermissionManagePrivateChannelMembers,
PermissionManagePublicChannelMembers,
PermissionDeletePrivateChannel,
@@ -911,6 +913,8 @@ func MakeDefaultRoles() map[string]*Role {
PermissionManagePublicChannelBanner.Id,
PermissionManagePrivateChannelBanner.Id,
PermissionManageChannelAccessRules.Id,
PermissionManagePublicChannelAutoTranslation.Id,
PermissionManagePrivateChannelAutoTranslation.Id,
},
SchemeManaged: true,
BuiltIn: true,
@@ -7,6 +7,7 @@ import {Link} from 'react-router-dom';
import type {AutoTranslationSettings} from '@mattermost/types/config';
import BooleanSetting from 'components/admin_console/boolean_setting';
import MultiSelectSetting from 'components/admin_console/multiselect_settings';
import Setting from 'components/admin_console/setting';
import {
@@ -283,6 +284,25 @@ export default function AutoTranslation(props: SystemConsoleCustomSettingsCompon
onChange={handleTimeoutChange}
disabled={props.disabled}
/>
<BooleanSetting
id='RestrictDMAndGM'
label={
<FormattedMessage
id='admin.site.localization.restrictDMAndGMTitle'
defaultMessage='Restrict auto-translation on direct messages and group messages'
/>
}
helpText={
<FormattedMessage
id='admin.site.localization.restrictDMAndGMDescription'
defaultMessage='By default, any member of a direct message or group message can enable auto-translation in those channels. If restricted, auto-translation will not be available in direct messages and group messages.'
/>
}
value={autoTranslationSettings.RestrictDMAndGM}
onChange={handleChange}
disabled={props.disabled || props.setByEnv}
setByEnv={props.setByEnv}
/>
</SectionContent>
}
</AdminSection>
@@ -64,6 +64,7 @@ exports[`components/admin_console/permission_schemes_settings/permission_tree sh
"permissions": Array [
"create_public_channel",
"manage_public_channel_properties",
"manage_public_channel_auto_translation",
Object {
"combined": true,
"id": "manage_public_channel_members_and_read_groups",
@@ -81,6 +82,7 @@ exports[`components/admin_console/permission_schemes_settings/permission_tree sh
"permissions": Array [
"create_private_channel",
"manage_private_channel_properties",
"manage_private_channel_auto_translation",
Object {
"combined": true,
"id": "manage_private_channel_members_and_read_groups",
@@ -258,6 +260,7 @@ exports[`components/admin_console/permission_schemes_settings/permission_tree sh
"permissions": Array [
"create_public_channel",
"manage_public_channel_properties",
"manage_public_channel_auto_translation",
Object {
"combined": true,
"id": "manage_public_channel_members_and_read_groups",
@@ -275,6 +278,7 @@ exports[`components/admin_console/permission_schemes_settings/permission_tree sh
"permissions": Array [
"create_private_channel",
"manage_private_channel_properties",
"manage_private_channel_auto_translation",
Object {
"combined": true,
"id": "manage_private_channel_members_and_read_groups",
@@ -479,6 +483,7 @@ exports[`components/admin_console/permission_schemes_settings/permission_tree sh
"permissions": Array [
"create_public_channel",
"manage_public_channel_properties",
"manage_public_channel_auto_translation",
Object {
"combined": true,
"id": "manage_public_channel_members_and_read_groups",
@@ -496,6 +501,7 @@ exports[`components/admin_console/permission_schemes_settings/permission_tree sh
"permissions": Array [
"create_private_channel",
"manage_private_channel_properties",
"manage_private_channel_auto_translation",
Object {
"combined": true,
"id": "manage_private_channel_members_and_read_groups",
@@ -689,6 +695,7 @@ exports[`components/admin_console/permission_schemes_settings/permission_tree sh
"permissions": Array [
"create_public_channel",
"manage_public_channel_properties",
"manage_public_channel_auto_translation",
Object {
"combined": true,
"id": "manage_public_channel_members_and_read_groups",
@@ -706,6 +713,7 @@ exports[`components/admin_console/permission_schemes_settings/permission_tree sh
"permissions": Array [
"create_private_channel",
"manage_private_channel_properties",
"manage_private_channel_auto_translation",
Object {
"combined": true,
"id": "manage_private_channel_members_and_read_groups",
@@ -910,6 +918,7 @@ exports[`components/admin_console/permission_schemes_settings/permission_tree sh
"permissions": Array [
"create_public_channel",
"manage_public_channel_properties",
"manage_public_channel_auto_translation",
Object {
"combined": true,
"id": "manage_public_channel_members_and_read_groups",
@@ -927,6 +936,7 @@ exports[`components/admin_console/permission_schemes_settings/permission_tree sh
"permissions": Array [
"create_private_channel",
"manage_private_channel_properties",
"manage_private_channel_auto_translation",
Object {
"combined": true,
"id": "manage_private_channel_members_and_read_groups",
@@ -1131,6 +1141,7 @@ exports[`components/admin_console/permission_schemes_settings/permission_tree sh
"permissions": Array [
"create_public_channel",
"manage_public_channel_properties",
"manage_public_channel_auto_translation",
Object {
"combined": true,
"id": "manage_public_channel_members_and_read_groups",
@@ -1148,6 +1159,7 @@ exports[`components/admin_console/permission_schemes_settings/permission_tree sh
"permissions": Array [
"create_private_channel",
"manage_private_channel_properties",
"manage_private_channel_auto_translation",
Object {
"combined": true,
"id": "manage_private_channel_members_and_read_groups",
@@ -1359,6 +1371,7 @@ exports[`components/admin_console/permission_schemes_settings/permission_tree sh
"permissions": Array [
"create_public_channel",
"manage_public_channel_properties",
"manage_public_channel_auto_translation",
Object {
"combined": true,
"id": "manage_public_channel_members_and_read_groups",
@@ -1376,6 +1389,7 @@ exports[`components/admin_console/permission_schemes_settings/permission_tree sh
"permissions": Array [
"create_private_channel",
"manage_private_channel_properties",
"manage_private_channel_auto_translation",
Object {
"combined": true,
"id": "manage_private_channel_members_and_read_groups",
@@ -87,6 +87,7 @@ export default class PermissionsTree extends React.PureComponent<Props, State> {
permissions: [
Permissions.CREATE_PUBLIC_CHANNEL,
Permissions.MANAGE_PUBLIC_CHANNEL_PROPERTIES,
Permissions.MANAGE_PUBLIC_CHANNEL_AUTO_TRANSLATION,
{
id: 'manage_public_channel_members_and_read_groups',
combined: true,
@@ -104,6 +105,7 @@ export default class PermissionsTree extends React.PureComponent<Props, State> {
permissions: [
Permissions.CREATE_PRIVATE_CHANNEL,
Permissions.MANAGE_PRIVATE_CHANNEL_PROPERTIES,
Permissions.MANAGE_PRIVATE_CHANNEL_AUTO_TRANSLATION,
{
id: 'manage_private_channel_members_and_read_groups',
combined: true,
@@ -275,6 +275,26 @@ export const permissionRolesStrings: Record<string, Record<string, MessageDescri
defaultMessage: 'Update public channel names, headers and purposes.',
},
}),
manage_public_channel_auto_translation: defineMessages({
name: {
id: 'admin.permissions.permission.manage_public_channel_auto_translation.name',
defaultMessage: 'Manage Channel Auto Translation',
},
description: {
id: 'admin.permissions.permission.manage_public_channel_auto_translation.description',
defaultMessage: 'Enable or disable auto translations for public channels.',
},
}),
manage_private_channel_auto_translation: defineMessages({
name: {
id: 'admin.permissions.permission.manage_private_channel_auto_translation.name',
defaultMessage: 'Manage Channel Auto Translation',
},
description: {
id: 'admin.permissions.permission.manage_private_channel_auto_translation.description',
defaultMessage: 'Enable or disable auto translations for private channels.',
},
}),
manage_roles: defineMessages({
name: {
id: 'admin.permissions.permission.manage_roles.name',
@@ -117,7 +117,7 @@ export type ChannelDetailsActions = {
getChannel: (channelId: string) => void;
getTeam: (teamId: string) => Promise<ActionResult>;
getChannelModerations: (channelId: string) => Promise<ActionResult>;
patchChannel: (channelId: string, patch: Channel) => Promise<ActionResult>;
patchChannel: (channelId: string, patch: Partial<Channel>) => Promise<ActionResult>;
updateChannelPrivacy: (channelId: string, privacy: string) => Promise<ActionResult>;
patchGroupSyncable: (groupID: string, syncableID: string, syncableType: SyncableType, patch: Partial<SyncablePatch>) => Promise<ActionResult>;
patchChannelModerations: (channelID: string, patch: ChannelModerationPatch[]) => Promise<ActionResult>;
@@ -556,7 +556,6 @@ export default class ChannelDetails extends React.PureComponent<ChannelDetailsPr
// Then patch the channel
const patchResult = await actions.patchChannel(channel.id, {
...channel,
group_constrained: isSynced,
});
@@ -169,7 +169,6 @@ describe('ChannelSettingsConfigurationTab', () => {
// Verify patchChannel was called with the updated values
expect(patchChannel).toHaveBeenCalledWith('channel1', {
...mockChannel,
banner_info: {
enabled: true,
text: 'New banner text',
@@ -397,7 +396,6 @@ describe('ChannelSettingsConfigurationTab', () => {
// Verify patchChannel was called with the trimmed values
expect(patchChannel).toHaveBeenCalledWith('channel1', {
...mockChannelWithBanner,
banner_info: {
enabled: true,
text: 'Banner text with whitespace', // Whitespace should be trimmed
@@ -150,9 +150,7 @@ function ChannelSettingsConfigurationTab({channel, setAreThereUnsavedChanges, sh
return false;
}
const updated: Channel = {
...channel,
};
const updated: Partial<Channel> = {};
updated.banner_info = {
text: updatedChannelBanner.text?.trim() || '',
@@ -201,7 +201,6 @@ describe('ChannelSettingsInfoTab', () => {
// Verify patchChannel was called with the updated values (without type change).
// Note: URL should remain unchanged when editing existing channels
expect(patchChannel).toHaveBeenCalledWith('channel1', {
...mockChannel,
display_name: 'Updated Channel Name',
name: 'test-channel', // URL should remain unchanged when editing existing channels
purpose: 'Updated purpose',
@@ -241,7 +240,6 @@ describe('ChannelSettingsInfoTab', () => {
// Verify patchChannel was called with the trimmed values
expect(patchChannel).toHaveBeenCalledWith('channel1', {
...mockChannel,
display_name: 'Channel Name With Whitespace', // Whitespace should be trimmed
name: 'test-channel', // URL should remain unchanged when editing existing channels
purpose: 'Purpose with whitespace', // Whitespace should be trimmed
@@ -229,8 +229,7 @@ function ChannelSettingsInfoTab({
}
// Build updated channel object
const updated: Channel = {
...channel,
const updated: Partial<Channel> = {
display_name: displayName.trim(),
name: channelUrl.trim(),
purpose: channelPurpose.trim(),
@@ -245,12 +244,12 @@ function ChannelSettingsInfoTab({
// After every successful save, update local state to match the saved values
// with this, we make sure that the unsavedChanges check will return false after saving
setDisplayName(data?.display_name ?? updated.display_name);
setChannelURL(data?.name ?? updated.name);
setChannelPurpose(data?.purpose ?? updated.purpose);
setChannelHeader(data?.header ?? updated.header);
setDisplayName(data?.display_name ?? updated.display_name ?? '');
setChannelURL(data?.name ?? updated.name ?? '');
setChannelPurpose(data?.purpose ?? updated.purpose ?? '');
setChannelHeader(data?.header ?? updated.header ?? '');
return true;
}, [channel, displayName, channelUrl, channelPurpose, channelHeader, channelType, setFormError, handleServerError]);
}, [channel, displayName, channelType, channelUrl, channelPurpose, channelHeader, dispatch, formatMessage, handleServerError]);
// Handle save changes panel actions
const handleSaveChanges = useCallback(async () => {
+6
View File
@@ -2032,10 +2032,14 @@
"admin.permissions.permission.manage_own_outgoing_webhooks.name": "Manage Own",
"admin.permissions.permission.manage_own_slash_commands.description": "Create, edit and delete your own slash commands.",
"admin.permissions.permission.manage_own_slash_commands.name": "Manage Own",
"admin.permissions.permission.manage_private_channel_auto_translation.description": "Enable or disable auto translations for private channels.",
"admin.permissions.permission.manage_private_channel_auto_translation.name": "Manage Channel Auto Translation",
"admin.permissions.permission.manage_private_channel_banner.description": "Enable, disable and edit channel banner.",
"admin.permissions.permission.manage_private_channel_banner.name": "Manage Channel Banner",
"admin.permissions.permission.manage_private_channel_properties.description": "Update private channel names, headers and purposes.",
"admin.permissions.permission.manage_private_channel_properties.name": "Manage Channel Settings",
"admin.permissions.permission.manage_public_channel_auto_translation.description": "Enable or disable auto translations for public channels.",
"admin.permissions.permission.manage_public_channel_auto_translation.name": "Manage Channel Auto Translation",
"admin.permissions.permission.manage_public_channel_banner.description": "Enable, disable and edit channel banner.",
"admin.permissions.permission.manage_public_channel_banner.name": "Manage Channel Banner",
"admin.permissions.permission.manage_public_channel_properties.description": "Update public channel names, headers and purposes.",
@@ -2871,6 +2875,8 @@
"admin.site.localization.goToAgentsConfig": "Go to Agents plugin config",
"admin.site.localization.languages.description": "Choose which languages should be the defaults",
"admin.site.localization.languages.title": "Languages",
"admin.site.localization.restrictDMAndGMDescription": "By default, any member of a direct message or group message can enable auto-translation in those channels. If restricted, auto-translation will not be available in direct messages and group messages.",
"admin.site.localization.restrictDMAndGMTitle": "Restrict auto-translation on direct messages and group messages",
"admin.site.localization.targetLanguagesDescription": "Choose which languages you'd like to make available for auto-translation.",
"admin.site.localization.targetLanguagesTitle": "Languages allowed",
"admin.site.move_thread": "Move Thread",
@@ -22,6 +22,8 @@ const values = {
CREATE_GROUP_CHANNEL: 'create_group_channel',
MANAGE_PUBLIC_CHANNEL_PROPERTIES: 'manage_public_channel_properties',
MANAGE_PRIVATE_CHANNEL_PROPERTIES: 'manage_private_channel_properties',
MANAGE_PUBLIC_CHANNEL_AUTO_TRANSLATION: 'manage_public_channel_auto_translation',
MANAGE_PRIVATE_CHANNEL_AUTO_TRANSLATION: 'manage_private_channel_auto_translation',
LIST_PUBLIC_TEAMS: 'list_public_teams',
JOIN_PUBLIC_TEAMS: 'join_public_teams',
LIST_PRIVATE_TEAMS: 'list_private_teams',
+2
View File
@@ -1113,6 +1113,8 @@ export const PermissionsScope = {
[Permissions.CREATE_GROUP_CHANNEL]: 'system_scope',
[Permissions.MANAGE_PUBLIC_CHANNEL_PROPERTIES]: 'channel_scope',
[Permissions.MANAGE_PRIVATE_CHANNEL_PROPERTIES]: 'channel_scope',
[Permissions.MANAGE_PUBLIC_CHANNEL_AUTO_TRANSLATION]: 'channel_scope',
[Permissions.MANAGE_PRIVATE_CHANNEL_AUTO_TRANSLATION]: 'channel_scope',
[Permissions.LIST_PUBLIC_TEAMS]: 'system_scope',
[Permissions.JOIN_PUBLIC_TEAMS]: 'system_scope',
[Permissions.LIST_PRIVATE_TEAMS]: 'system_scope',
+5
View File
@@ -238,6 +238,10 @@ export type ClientConfig = {
EnableAttributeBasedAccessControl: string;
EnableChannelScopeAccessControl: string;
EnableUserManagedAttributes: string;
// Auto Translation Settings
EnableAutoTranslation: string;
RestrictDMAndGMAutotranslation: string;
};
export type License = {
@@ -765,6 +769,7 @@ export type AutoTranslationSettings = {
LLMServiceID: string;
};
TimeoutMs: number;
RestrictDMAndGM: boolean;
};
export type SamlSettings = {