diff --git a/api/v4/source/channels.yaml b/api/v4/source/channels.yaml
index 33a1db66b90..401133e83bf 100644
--- a/api/v4/source/channels.yaml
+++ b/api/v4/source/channels.yaml
@@ -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":
diff --git a/e2e-tests/cypress/tests/integration/channels/channel/channel_info_rhs_spec.ts b/e2e-tests/cypress/tests/integration/channels/channel/channel_info_rhs_spec.ts
index 6f345a3e0ca..9a9dc478ec2 100644
--- a/e2e-tests/cypress/tests/integration/channels/channel/channel_info_rhs_spec.ts
+++ b/e2e-tests/cypress/tests/integration/channels/channel/channel_info_rhs_spec.ts
@@ -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');
diff --git a/e2e-tests/cypress/tests/support/api/role.js b/e2e-tests/cypress/tests/support/api/role.js
index bc6dfff5d0d..8398c39c5fe 100644
--- a/e2e-tests/cypress/tests/support/api/role.js
+++ b/e2e-tests/cypress/tests/support/api/role.js
@@ -9,7 +9,7 @@ import xor from 'lodash.xor';
// *****************************************************************************
export const defaultRolesPermissions = {
- channel_admin: 'use_channel_mentions remove_reaction manage_public_channel_members use_group_mentions manage_channel_roles manage_private_channel_members add_reaction read_public_channel_groups create_post read_private_channel_groups add_bookmark_public_channel edit_bookmark_public_channel delete_bookmark_public_channel order_bookmark_public_channel add_bookmark_private_channel edit_bookmark_private_channel delete_bookmark_private_channel order_bookmark_private_channel',
+ channel_admin: 'use_channel_mentions remove_reaction manage_public_channel_members use_group_mentions manage_channel_roles manage_private_channel_members add_reaction read_public_channel_groups create_post read_private_channel_groups add_bookmark_public_channel edit_bookmark_public_channel delete_bookmark_public_channel order_bookmark_public_channel add_bookmark_private_channel edit_bookmark_private_channel delete_bookmark_private_channel order_bookmark_private_channel manage_public_channel_auto_translation manage_private_channel_auto_translation',
channel_guest: 'upload_file edit_post create_post use_channel_mentions read_channel read_channel_content add_reaction remove_reaction',
channel_user: 'manage_private_channel_members read_public_channel_groups delete_post read_private_channel_groups use_group_mentions manage_private_channel_properties delete_public_channel add_reaction manage_public_channel_properties edit_post upload_file use_channel_mentions get_public_link read_channel read_channel_content delete_private_channel manage_public_channel_members create_post remove_reaction add_bookmark_public_channel edit_bookmark_public_channel delete_bookmark_public_channel order_bookmark_public_channel add_bookmark_private_channel edit_bookmark_private_channel delete_bookmark_private_channel order_bookmark_private_channel',
custom_group_user: '',
@@ -17,7 +17,7 @@ export const defaultRolesPermissions = {
playbook_member: 'playbook_public_view playbook_public_manage_members playbook_public_manage_properties playbook_private_view playbook_private_manage_members playbook_private_manage_properties run_create',
run_admin: 'run_manage_properties run_manage_members',
run_member: 'run_view',
- system_admin: 'sysconsole_write_environment_elasticsearch playbook_public_manage_properties sysconsole_write_authentication_ldap run_view manage_jobs manage_roles playbook_public_create manage_public_channel_properties sysconsole_read_plugins delete_post purge_elasticsearch_indexes sysconsole_read_integrations_bot_accounts read_data_retention_job manage_private_channel_members create_elasticsearch_post_indexing_job manage_elasticsearch_post_indexing_job sysconsole_read_authentication_guest_access create_elasticsearch_post_aggregation_job manage_elasticsearch_post_aggregation_job join_public_teams sysconsole_read_site_public_links add_saml_idp_cert sysconsole_write_site_announcement_banner sysconsole_write_site_notices sysconsole_read_experimental_feature_flags sysconsole_read_site_users_and_teams manage_own_slash_commands manage_others_slash_commands sysconsole_read_authentication_ldap read_channel read_channel_content sysconsole_write_authentication_password list_users_without_team sysconsole_read_authentication_email add_saml_public_cert playbook_private_create promote_guest sysconsole_read_user_management_system_roles manage_public_channel_members create_data_retention_job manage_data_retention_job add_saml_private_cert sysconsole_write_user_management_users sysconsole_read_compliance_compliance_monitoring playbook_public_manage_members sysconsole_write_environment_database sysconsole_write_user_management_teams playbook_private_manage_roles read_public_channel sysconsole_write_plugins sysconsole_read_authentication_openid sysconsole_write_user_management_groups sysconsole_write_site_file_sharing_and_downloads playbook_private_manage_properties sysconsole_read_site_customization join_public_channels add_user_to_team restore_custom_group download_compliance_export_result sysconsole_write_user_management_system_roles sysconsole_write_environment_session_lengths create_custom_group manage_private_channel_properties create_post_public remove_ldap_private_cert sysconsole_write_site_public_links import_team sysconsole_read_environment_developer sysconsole_read_environment_database sysconsole_read_environment_web_server sysconsole_read_environment_mobile_security sysconsole_write_environment_mobile_security use_channel_mentions view_team remove_others_reactions sysconsole_read_environment_session_lengths sysconsole_write_integrations_bot_accounts playbook_public_view use_group_mentions sysconsole_write_environment_web_server add_ldap_private_cert read_public_channel_groups invite_guest sysconsole_read_environment_smtp create_post sysconsole_read_about_edition_and_license sysconsole_read_authentication_signup sysconsole_read_authentication_saml sysconsole_read_environment_file_storage sysconsole_write_experimental_feature_flags sysconsole_write_site_localization sysconsole_write_environment_rate_limiting sysconsole_read_environment_rate_limiting sysconsole_read_products_boards get_saml_cert_status sysconsole_read_environment_high_availability manage_secure_connections read_compliance_export_job sysconsole_write_compliance_custom_terms_of_service read_user_access_token edit_post sysconsole_write_environment_logging sysconsole_read_environment_push_notification_server sysconsole_write_site_customization read_other_users_teams read_elasticsearch_post_aggregation_job sysconsole_write_compliance_data_retention_policy sysconsole_read_user_management_permissions sysconsole_read_site_emoji sysconsole_read_compliance_data_retention_policy read_license_information sysconsole_read_experimental_features read_deleted_posts sysconsole_read_environment_logging sysconsole_read_reporting_site_statistics test_elasticsearch sysconsole_read_site_posts add_reaction sysconsole_write_authentication_signup manage_own_outgoing_webhooks manage_others_outgoing_webhooks create_post_ephemeral sysconsole_read_environment_image_proxy invite_user create_user_access_token sysconsole_write_environment_image_proxy sysconsole_write_products_boards read_elasticsearch_post_indexing_job sysconsole_write_environment_performance_monitoring sysconsole_write_authentication_guest_access sysconsole_read_compliance_custom_terms_of_service edit_others_posts sysconsole_write_billing get_saml_metadata_from_idp sysconsole_write_authentication_saml invalidate_caches view_members manage_others_bots run_create join_private_teams convert_private_channel_to_public read_audits assign_bot read_jobs remove_user_from_team revoke_user_access_token manage_team sysconsole_read_reporting_server_logs get_public_link manage_system delete_public_channel read_private_channel_groups sysconsole_read_authentication_mfa delete_emojis list_private_teams create_emojis sysconsole_read_billing sysconsole_write_site_emoji invalidate_email_invite sysconsole_write_environment_file_storage sysconsole_write_compliance_compliance_monitoring remove_saml_public_cert sysconsole_read_compliance_compliance_export sysconsole_read_site_localization manage_team_roles list_public_teams get_logs sysconsole_write_integrations_integration_management sysconsole_read_integrations_cors manage_oauth manage_outgoing_oauth_connections delete_others_emojis sysconsole_write_integrations_gif manage_own_incoming_webhooks manage_others_incoming_webhooks bypass_incoming_webhook_channel_lock sysconsole_write_authentication_email create_private_channel playbook_private_make_public manage_bots add_ldap_public_cert remove_ldap_public_cert sysconsole_write_site_notifications sysconsole_write_environment_developer playbook_private_manage_members sysconsole_read_user_management_teams edit_custom_group remove_reaction playbook_public_manage_roles sysconsole_write_reporting_server_logs read_others_bots sysconsole_write_site_posts sysconsole_read_site_notifications sysconsole_read_authentication_password playbook_private_view manage_system_wide_oauth get_analytics list_team_channels sysconsole_write_user_management_channels delete_private_channel manage_custom_group_members test_s3 create_ldap_sync_job manage_ldap_sync_job sysconsole_read_integrations_integration_management test_site_url recycle_database_connections sysconsole_read_site_announcement_banner test_email manage_shared_channels read_bots sysconsole_write_environment_smtp sysconsole_write_environment_push_notification_server sysconsole_write_user_management_permissions sysconsole_read_environment_elasticsearch sysconsole_write_reporting_site_statistics sysconsole_write_site_users_and_teams demote_to_guest create_team test_ldap remove_saml_idp_cert delete_others_posts edit_other_users sysconsole_write_reporting_team_statistics sysconsole_read_integrations_gif sysconsole_read_site_notices sysconsole_write_about_edition_and_license run_manage_members create_bot sysconsole_write_authentication_mfa sysconsole_read_user_management_users assign_system_admin_role sysconsole_write_experimental_features edit_brand create_group_channel sysconsole_write_authentication_openid create_direct_channel manage_license_information reload_config manage_channel_roles sysconsole_read_user_management_groups create_compliance_export_job manage_compliance_export_job read_ldap_sync_job upload_file sysconsole_read_site_file_sharing_and_downloads delete_custom_group sysconsole_read_user_management_channels sysconsole_write_compliance_compliance_export remove_saml_private_cert sysconsole_read_environment_performance_monitoring create_public_channel sysconsole_write_integrations_cors sysconsole_write_environment_high_availability playbook_public_make_private run_manage_properties sysconsole_read_reporting_team_statistics convert_public_channel_to_private add_bookmark_public_channel edit_bookmark_public_channel delete_bookmark_public_channel order_bookmark_public_channel add_bookmark_private_channel edit_bookmark_private_channel delete_bookmark_private_channel order_bookmark_private_channel',
+ system_admin: 'sysconsole_write_environment_elasticsearch playbook_public_manage_properties sysconsole_write_authentication_ldap run_view manage_jobs manage_roles playbook_public_create manage_public_channel_properties manage_public_channel_auto_translation manage_private_channel_auto_translation sysconsole_read_plugins delete_post purge_elasticsearch_indexes sysconsole_read_integrations_bot_accounts read_data_retention_job manage_private_channel_members create_elasticsearch_post_indexing_job manage_elasticsearch_post_indexing_job sysconsole_read_authentication_guest_access create_elasticsearch_post_aggregation_job manage_elasticsearch_post_aggregation_job join_public_teams sysconsole_read_site_public_links add_saml_idp_cert sysconsole_write_site_announcement_banner sysconsole_write_site_notices sysconsole_read_experimental_feature_flags sysconsole_read_site_users_and_teams manage_own_slash_commands manage_others_slash_commands sysconsole_read_authentication_ldap read_channel read_channel_content sysconsole_write_authentication_password list_users_without_team sysconsole_read_authentication_email add_saml_public_cert playbook_private_create promote_guest sysconsole_read_user_management_system_roles manage_public_channel_members create_data_retention_job manage_data_retention_job add_saml_private_cert sysconsole_write_user_management_users sysconsole_read_compliance_compliance_monitoring playbook_public_manage_members sysconsole_write_environment_database sysconsole_write_user_management_teams playbook_private_manage_roles read_public_channel sysconsole_write_plugins sysconsole_read_authentication_openid sysconsole_write_user_management_groups sysconsole_write_site_file_sharing_and_downloads playbook_private_manage_properties sysconsole_read_site_customization join_public_channels add_user_to_team restore_custom_group download_compliance_export_result sysconsole_write_user_management_system_roles sysconsole_write_environment_session_lengths create_custom_group manage_private_channel_properties create_post_public remove_ldap_private_cert sysconsole_write_site_public_links import_team sysconsole_read_environment_developer sysconsole_read_environment_database sysconsole_read_environment_web_server sysconsole_read_environment_mobile_security sysconsole_write_environment_mobile_security use_channel_mentions view_team remove_others_reactions sysconsole_read_environment_session_lengths sysconsole_write_integrations_bot_accounts playbook_public_view use_group_mentions sysconsole_write_environment_web_server add_ldap_private_cert read_public_channel_groups invite_guest sysconsole_read_environment_smtp create_post sysconsole_read_about_edition_and_license sysconsole_read_authentication_signup sysconsole_read_authentication_saml sysconsole_read_environment_file_storage sysconsole_write_experimental_feature_flags sysconsole_write_site_localization sysconsole_write_environment_rate_limiting sysconsole_read_environment_rate_limiting sysconsole_read_products_boards get_saml_cert_status sysconsole_read_environment_high_availability manage_secure_connections read_compliance_export_job sysconsole_write_compliance_custom_terms_of_service read_user_access_token edit_post sysconsole_write_environment_logging sysconsole_read_environment_push_notification_server sysconsole_write_site_customization read_other_users_teams read_elasticsearch_post_aggregation_job sysconsole_write_compliance_data_retention_policy sysconsole_read_user_management_permissions sysconsole_read_site_emoji sysconsole_read_compliance_data_retention_policy read_license_information sysconsole_read_experimental_features read_deleted_posts sysconsole_read_environment_logging sysconsole_read_reporting_site_statistics test_elasticsearch sysconsole_read_site_posts add_reaction sysconsole_write_authentication_signup manage_own_outgoing_webhooks manage_others_outgoing_webhooks create_post_ephemeral sysconsole_read_environment_image_proxy invite_user create_user_access_token sysconsole_write_environment_image_proxy sysconsole_write_products_boards read_elasticsearch_post_indexing_job sysconsole_write_environment_performance_monitoring sysconsole_write_authentication_guest_access sysconsole_read_compliance_custom_terms_of_service edit_others_posts sysconsole_write_billing get_saml_metadata_from_idp sysconsole_write_authentication_saml invalidate_caches view_members manage_others_bots run_create join_private_teams convert_private_channel_to_public read_audits assign_bot read_jobs remove_user_from_team revoke_user_access_token manage_team sysconsole_read_reporting_server_logs get_public_link manage_system delete_public_channel read_private_channel_groups sysconsole_read_authentication_mfa delete_emojis list_private_teams create_emojis sysconsole_read_billing sysconsole_write_site_emoji invalidate_email_invite sysconsole_write_environment_file_storage sysconsole_write_compliance_compliance_monitoring remove_saml_public_cert sysconsole_read_compliance_compliance_export sysconsole_read_site_localization manage_team_roles list_public_teams get_logs sysconsole_write_integrations_integration_management sysconsole_read_integrations_cors manage_oauth manage_outgoing_oauth_connections delete_others_emojis sysconsole_write_integrations_gif manage_own_incoming_webhooks manage_others_incoming_webhooks bypass_incoming_webhook_channel_lock sysconsole_write_authentication_email create_private_channel playbook_private_make_public manage_bots add_ldap_public_cert remove_ldap_public_cert sysconsole_write_site_notifications sysconsole_write_environment_developer playbook_private_manage_members sysconsole_read_user_management_teams edit_custom_group remove_reaction playbook_public_manage_roles sysconsole_write_reporting_server_logs read_others_bots sysconsole_write_site_posts sysconsole_read_site_notifications sysconsole_read_authentication_password playbook_private_view manage_system_wide_oauth get_analytics list_team_channels sysconsole_write_user_management_channels delete_private_channel manage_custom_group_members test_s3 create_ldap_sync_job manage_ldap_sync_job sysconsole_read_integrations_integration_management test_site_url recycle_database_connections sysconsole_read_site_announcement_banner test_email manage_shared_channels read_bots sysconsole_write_environment_smtp sysconsole_write_environment_push_notification_server sysconsole_write_user_management_permissions sysconsole_read_environment_elasticsearch sysconsole_write_reporting_site_statistics sysconsole_write_site_users_and_teams demote_to_guest create_team test_ldap remove_saml_idp_cert delete_others_posts edit_other_users sysconsole_write_reporting_team_statistics sysconsole_read_integrations_gif sysconsole_read_site_notices sysconsole_write_about_edition_and_license run_manage_members create_bot sysconsole_write_authentication_mfa sysconsole_read_user_management_users assign_system_admin_role sysconsole_write_experimental_features edit_brand create_group_channel sysconsole_write_authentication_openid create_direct_channel manage_license_information reload_config manage_channel_roles sysconsole_read_user_management_groups create_compliance_export_job manage_compliance_export_job read_ldap_sync_job upload_file sysconsole_read_site_file_sharing_and_downloads delete_custom_group sysconsole_read_user_management_channels sysconsole_write_compliance_compliance_export remove_saml_private_cert sysconsole_read_environment_performance_monitoring create_public_channel sysconsole_write_integrations_cors sysconsole_write_environment_high_availability playbook_public_make_private run_manage_properties sysconsole_read_reporting_team_statistics convert_public_channel_to_private add_bookmark_public_channel edit_bookmark_public_channel delete_bookmark_public_channel order_bookmark_public_channel add_bookmark_private_channel edit_bookmark_private_channel delete_bookmark_private_channel order_bookmark_private_channel',
system_custom_group_admin: 'create_custom_group edit_custom_group delete_custom_group restore_custom_group manage_custom_group_members',
system_guest: 'create_group_channel create_direct_channel',
system_manager: 'sysconsole_read_site_announcement_banner manage_private_channel_properties edit_brand read_private_channel_groups manage_private_channel_members manage_team_roles sysconsole_write_environment_session_lengths sysconsole_read_site_emoji sysconsole_write_environment_developer sysconsole_read_user_management_groups sysconsole_write_user_management_groups sysconsole_write_environment_rate_limiting delete_private_channel sysconsole_read_environment_performance_monitoring sysconsole_read_environment_rate_limiting sysconsole_write_user_management_teams sysconsole_write_integrations_integration_management sysconsole_write_site_public_links sysconsole_read_authentication_ldap sysconsole_write_integrations_cors reload_config sysconsole_write_user_management_channels sysconsole_read_environment_high_availability sysconsole_read_site_users_and_teams sysconsole_read_user_management_teams sysconsole_write_site_users_and_teams sysconsole_read_site_customization sysconsole_write_environment_high_availability sysconsole_read_integrations_bot_accounts sysconsole_read_authentication_guest_access sysconsole_read_site_public_links read_elasticsearch_post_indexing_job sysconsole_read_user_management_channels sysconsole_read_reporting_team_statistics invalidate_caches sysconsole_read_authentication_signup read_elasticsearch_post_aggregation_job sysconsole_write_environment_smtp manage_public_channel_members list_public_teams add_user_to_team sysconsole_read_environment_web_server sysconsole_read_site_localization get_logs sysconsole_write_site_posts sysconsole_write_integrations_bot_accounts sysconsole_write_user_management_permissions sysconsole_read_environment_elasticsearch sysconsole_read_environment_smtp list_private_teams read_public_channel_groups sysconsole_write_environment_file_storage sysconsole_write_integrations_gif manage_public_channel_properties sysconsole_write_environment_performance_monitoring sysconsole_write_site_notifications sysconsole_read_site_notifications sysconsole_read_environment_image_proxy sysconsole_write_site_announcement_banner sysconsole_write_site_emoji test_site_url sysconsole_read_integrations_gif sysconsole_write_environment_logging convert_public_channel_to_private get_analytics sysconsole_read_user_management_permissions sysconsole_write_environment_image_proxy test_elasticsearch recycle_database_connections sysconsole_write_site_localization sysconsole_read_reporting_server_logs create_elasticsearch_post_indexing_job manage_elasticsearch_post_indexing_job sysconsole_read_reporting_site_statistics test_ldap delete_public_channel sysconsole_write_environment_push_notification_server read_license_information sysconsole_write_products_boards sysconsole_read_about_edition_and_license convert_private_channel_to_public sysconsole_read_integrations_integration_management create_elasticsearch_post_aggregation_job manage_elasticsearch_post_aggregation_job purge_elasticsearch_indexes sysconsole_read_environment_database join_public_teams sysconsole_read_authentication_email sysconsole_read_environment_push_notification_server view_team read_channel sysconsole_read_authentication_password read_ldap_sync_job sysconsole_read_integrations_cors sysconsole_read_environment_logging manage_team sysconsole_read_authentication_openid read_public_channel sysconsole_write_environment_elasticsearch sysconsole_read_plugins manage_channel_roles remove_user_from_team test_email sysconsole_write_site_file_sharing_and_downloads test_s3 sysconsole_read_site_file_sharing_and_downloads sysconsole_read_site_notices sysconsole_read_environment_file_storage join_private_teams sysconsole_read_products_boards sysconsole_read_environment_session_lengths sysconsole_write_environment_database sysconsole_read_authentication_saml sysconsole_read_authentication_mfa sysconsole_write_site_notices sysconsole_write_environment_web_server sysconsole_read_site_posts sysconsole_read_environment_developer sysconsole_write_site_customization sysconsole_read_environment_mobile_security sysconsole_write_environment_mobile_security manage_outgoing_oauth_connections',
diff --git a/e2e-tests/playwright/lib/src/server/default_config.ts b/e2e-tests/playwright/lib/src/server/default_config.ts
index b6fdbe35e4d..a5f45fa94fa 100644
--- a/e2e-tests/playwright/lib/src/server/default_config.ts
+++ b/e2e-tests/playwright/lib/src/server/default_config.ts
@@ -854,5 +854,6 @@ const defaultServerConfig: AdminConfig = {
Agents: {
LLMServiceID: '',
},
+ RestrictDMAndGM: false,
},
};
diff --git a/server/channels/api4/channel.go b/server/channels/api4/channel.go
index c7a064246b1..2afc9012e30 100644
--- a/server/channels/api4/channel.go
+++ b/server/channels/api4/channel.go
@@ -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
diff --git a/server/channels/api4/channel_test.go b/server/channels/api4/channel_test.go
index 8246ab1f27a..49b0c562b78 100644
--- a/server/channels/api4/channel_test.go
+++ b/server/channels/api4/channel_test.go
@@ -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) {
diff --git a/server/channels/app/app_test.go b/server/channels/app/app_test.go
index 2dc579994a1..7fc7255c53a 100644
--- a/server/channels/app/app_test.go
+++ b/server/channels/app/app_test.go
@@ -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,
diff --git a/server/channels/app/channel.go b/server/channels/app/channel.go
index 13684942029..63c53016abf 100644
--- a/server/channels/app/channel.go
+++ b/server/channels/app/channel.go
@@ -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 {
diff --git a/server/channels/app/channel_test.go b/server/channels/app/channel_test.go
index 0ffc34ef463..c9f2cf39735 100644
--- a/server/channels/app/channel_test.go
+++ b/server/channels/app/channel_test.go
@@ -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) {
diff --git a/server/channels/app/permissions_migrations.go b/server/channels/app/permissions_migrations.go
index 965f6c0de04..19944b2612e 100644
--- a/server/channels/app/permissions_migrations.go
+++ b/server/channels/app/permissions_migrations.go
@@ -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()
diff --git a/server/channels/store/localcachelayer/autotranslation_layer.go b/server/channels/store/localcachelayer/autotranslation_layer.go
index 1c2aee42a88..0e7fd7673a9 100644
--- a/server/channels/store/localcachelayer/autotranslation_layer.go
+++ b/server/channels/store/localcachelayer/autotranslation_layer.go
@@ -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)
diff --git a/server/channels/store/retrylayer/retrylayer.go b/server/channels/store/retrylayer/retrylayer.go
index 0924f1370d4..ecbc8bf09cf 100644
--- a/server/channels/store/retrylayer/retrylayer.go
+++ b/server/channels/store/retrylayer/retrylayer.go
@@ -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
diff --git a/server/channels/store/sqlstore/autotranslation_store.go b/server/channels/store/sqlstore/autotranslation_store.go
index 3959e0901d2..22e9fbba013 100644
--- a/server/channels/store/sqlstore/autotranslation_store.go
+++ b/server/channels/store/sqlstore/autotranslation_store.go
@@ -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
diff --git a/server/channels/store/sqlstore/channel_store.go b/server/channels/store/sqlstore/channel_store.go
index 37036242b8e..507f9a41c69 100644
--- a/server/channels/store/sqlstore/channel_store.go
+++ b/server/channels/store/sqlstore/channel_store.go
@@ -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"}) {
diff --git a/server/channels/store/store.go b/server/channels/store/store.go
index 7f2c6b06cf1..f490595856e 100644
--- a/server/channels/store/store.go
+++ b/server/channels/store/store.go
@@ -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.
diff --git a/server/channels/store/storetest/autotranslation.go b/server/channels/store/storetest/autotranslation.go
index c0abc269122..d1b5a80ce37 100644
--- a/server/channels/store/storetest/autotranslation.go
+++ b/server/channels/store/storetest/autotranslation.go
@@ -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}
diff --git a/server/channels/store/storetest/mocks/AutoTranslationStore.go b/server/channels/store/storetest/mocks/AutoTranslationStore.go
index 20656267db3..10ea7a1b94a 100644
--- a/server/channels/store/storetest/mocks/AutoTranslationStore.go
+++ b/server/channels/store/storetest/mocks/AutoTranslationStore.go
@@ -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)
diff --git a/server/channels/store/timerlayer/timerlayer.go b/server/channels/store/timerlayer/timerlayer.go
index e21022758e3..7a0c94a2bac 100644
--- a/server/channels/store/timerlayer/timerlayer.go
+++ b/server/channels/store/timerlayer/timerlayer.go
@@ -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()
diff --git a/server/channels/testlib/store.go b/server/channels/testlib/store.go
index 308a155e95a..b4064c298c5 100644
--- a/server/channels/testlib/store.go
+++ b/server/channels/testlib/store.go
@@ -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)
diff --git a/server/cmd/mmctl/commands/permissions_test.go b/server/cmd/mmctl/commands/permissions_test.go
index 8b6d4907ec3..51cbd4f5332 100644
--- a/server/cmd/mmctl/commands/permissions_test.go
+++ b/server/cmd/mmctl/commands/permissions_test.go
@@ -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,
diff --git a/server/config/client.go b/server/config/client.go
index 0f854757361..0d18daaa64b 100644
--- a/server/config/client.go
+++ b/server/config/client.go
@@ -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)
}
}
diff --git a/server/einterfaces/autotranslation.go b/server/einterfaces/autotranslation.go
index ad1504834c4..26a302f3504 100644
--- a/server/einterfaces/autotranslation.go
+++ b/server/einterfaces/autotranslation.go
@@ -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.
diff --git a/server/einterfaces/mocks/AutoTranslationInterface.go b/server/einterfaces/mocks/AutoTranslationInterface.go
index beecafb862a..b0d41bf50b1 100644
--- a/server/einterfaces/mocks/AutoTranslationInterface.go
+++ b/server/einterfaces/mocks/AutoTranslationInterface.go
@@ -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)
diff --git a/server/i18n/en.json b/server/i18n/en.json
index 22100988305..817f95dd412 100644
--- a/server/i18n/en.json
+++ b/server/i18n/en.json
@@ -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"
diff --git a/server/public/model/channel.go b/server/public/model/channel.go
index e174288b83d..8730449fa1a 100644
--- a/server/public/model/channel.go
+++ b/server/public/model/channel.go
@@ -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() {
diff --git a/server/public/model/config.go b/server/public/model/config.go
index c9cd407f9fd..4c314681cf3 100644
--- a/server/public/model/config.go
+++ b/server/public/model/config.go
@@ -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() {
diff --git a/server/public/model/migration.go b/server/public/model/migration.go
index a77785d62bd..abb7bbdb020 100644
--- a/server/public/model/migration.go
+++ b/server/public/model/migration.go
@@ -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"
)
diff --git a/server/public/model/permission.go b/server/public/model/permission.go
index 2393c0c2f89..12d5a005513 100644
--- a/server/public/model/permission.go
+++ b/server/public/model/permission.go
@@ -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,
diff --git a/server/public/model/post.go b/server/public/model/post.go
index de662f4c532..ea9b650ea32 100644
--- a/server/public/model/post.go
+++ b/server/public/model/post.go
@@ -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) {
diff --git a/server/public/model/role.go b/server/public/model/role.go
index bbd17f3b0ae..bdfaf7e3f97 100644
--- a/server/public/model/role.go
+++ b/server/public/model/role.go
@@ -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,
diff --git a/webapp/channels/src/components/admin_console/localization/auto_translation.tsx b/webapp/channels/src/components/admin_console/localization/auto_translation.tsx
index d5e85233af5..df762c5120d 100644
--- a/webapp/channels/src/components/admin_console/localization/auto_translation.tsx
+++ b/webapp/channels/src/components/admin_console/localization/auto_translation.tsx
@@ -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}
/>
+
+ }
+ helpText={
+
+ }
+ value={autoTranslationSettings.RestrictDMAndGM}
+ onChange={handleChange}
+ disabled={props.disabled || props.setByEnv}
+ setByEnv={props.setByEnv}
+ />
}
diff --git a/webapp/channels/src/components/admin_console/permission_schemes_settings/permissions_tree/__snapshots__/permissions_tree.test.tsx.snap b/webapp/channels/src/components/admin_console/permission_schemes_settings/permissions_tree/__snapshots__/permissions_tree.test.tsx.snap
index dfd509ad3f4..44f4fa4e54e 100644
--- a/webapp/channels/src/components/admin_console/permission_schemes_settings/permissions_tree/__snapshots__/permissions_tree.test.tsx.snap
+++ b/webapp/channels/src/components/admin_console/permission_schemes_settings/permissions_tree/__snapshots__/permissions_tree.test.tsx.snap
@@ -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",
diff --git a/webapp/channels/src/components/admin_console/permission_schemes_settings/permissions_tree/permissions_tree.tsx b/webapp/channels/src/components/admin_console/permission_schemes_settings/permissions_tree/permissions_tree.tsx
index 9aa3d363f60..b41a889b64e 100644
--- a/webapp/channels/src/components/admin_console/permission_schemes_settings/permissions_tree/permissions_tree.tsx
+++ b/webapp/channels/src/components/admin_console/permission_schemes_settings/permissions_tree/permissions_tree.tsx
@@ -87,6 +87,7 @@ export default class PermissionsTree extends React.PureComponent {
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 {
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,
diff --git a/webapp/channels/src/components/admin_console/permission_schemes_settings/strings/permissions.tsx b/webapp/channels/src/components/admin_console/permission_schemes_settings/strings/permissions.tsx
index 4c7ac290b1b..e7dea308eb4 100644
--- a/webapp/channels/src/components/admin_console/permission_schemes_settings/strings/permissions.tsx
+++ b/webapp/channels/src/components/admin_console/permission_schemes_settings/strings/permissions.tsx
@@ -275,6 +275,26 @@ export const permissionRolesStrings: Record void;
getTeam: (teamId: string) => Promise;
getChannelModerations: (channelId: string) => Promise;
- patchChannel: (channelId: string, patch: Channel) => Promise;
+ patchChannel: (channelId: string, patch: Partial) => Promise;
updateChannelPrivacy: (channelId: string, privacy: string) => Promise;
patchGroupSyncable: (groupID: string, syncableID: string, syncableType: SyncableType, patch: Partial) => Promise;
patchChannelModerations: (channelID: string, patch: ChannelModerationPatch[]) => Promise;
@@ -556,7 +556,6 @@ export default class ChannelDetails extends React.PureComponent {
// 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
diff --git a/webapp/channels/src/components/channel_settings_modal/channel_settings_configuration_tab.tsx b/webapp/channels/src/components/channel_settings_modal/channel_settings_configuration_tab.tsx
index 32e1facc558..28a7796fd07 100644
--- a/webapp/channels/src/components/channel_settings_modal/channel_settings_configuration_tab.tsx
+++ b/webapp/channels/src/components/channel_settings_modal/channel_settings_configuration_tab.tsx
@@ -150,9 +150,7 @@ function ChannelSettingsConfigurationTab({channel, setAreThereUnsavedChanges, sh
return false;
}
- const updated: Channel = {
- ...channel,
- };
+ const updated: Partial = {};
updated.banner_info = {
text: updatedChannelBanner.text?.trim() || '',
diff --git a/webapp/channels/src/components/channel_settings_modal/channel_settings_info_tab.test.tsx b/webapp/channels/src/components/channel_settings_modal/channel_settings_info_tab.test.tsx
index 6ab2e224606..bf519280399 100644
--- a/webapp/channels/src/components/channel_settings_modal/channel_settings_info_tab.test.tsx
+++ b/webapp/channels/src/components/channel_settings_modal/channel_settings_info_tab.test.tsx
@@ -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
diff --git a/webapp/channels/src/components/channel_settings_modal/channel_settings_info_tab.tsx b/webapp/channels/src/components/channel_settings_modal/channel_settings_info_tab.tsx
index 32aa9078c5e..9ebecbe9ee6 100644
--- a/webapp/channels/src/components/channel_settings_modal/channel_settings_info_tab.tsx
+++ b/webapp/channels/src/components/channel_settings_modal/channel_settings_info_tab.tsx
@@ -229,8 +229,7 @@ function ChannelSettingsInfoTab({
}
// Build updated channel object
- const updated: Channel = {
- ...channel,
+ const updated: Partial = {
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 () => {
diff --git a/webapp/channels/src/i18n/en.json b/webapp/channels/src/i18n/en.json
index cb21d59ffd5..4c2d27a9b76 100644
--- a/webapp/channels/src/i18n/en.json
+++ b/webapp/channels/src/i18n/en.json
@@ -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",
diff --git a/webapp/channels/src/packages/mattermost-redux/src/constants/permissions.ts b/webapp/channels/src/packages/mattermost-redux/src/constants/permissions.ts
index 366f050fadf..af3ae93b283 100644
--- a/webapp/channels/src/packages/mattermost-redux/src/constants/permissions.ts
+++ b/webapp/channels/src/packages/mattermost-redux/src/constants/permissions.ts
@@ -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',
diff --git a/webapp/channels/src/utils/constants.tsx b/webapp/channels/src/utils/constants.tsx
index ba537409428..09aee85ed84 100644
--- a/webapp/channels/src/utils/constants.tsx
+++ b/webapp/channels/src/utils/constants.tsx
@@ -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',
diff --git a/webapp/platform/types/src/config.ts b/webapp/platform/types/src/config.ts
index 718c5f9f4a6..d0ae01cf03c 100644
--- a/webapp/platform/types/src/config.ts
+++ b/webapp/platform/types/src/config.ts
@@ -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 = {