From 2bd29c03593643821144d59ae57725c30db7a07a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Espino=20Garc=C3=ADa?= Date: Fri, 6 Feb 2026 18:19:06 +0100 Subject: [PATCH] Add the ability to patch channel autotranslations (#35078) * Add the ability to patch channel autotranslations * Fix lint * Update docs * Fix CI * Fix CI * Fix mmctl test * Check whether the channel is translated for the user when checking user enabled * Fix wrong uses of patch acrros e2e and frontend * Fix test * Fix wording * Fix tests and column name * Move group constrained test so they don't mess with the basic entities * Fix patch sending too much information --- api/v4/source/channels.yaml | 50 +- .../channels/channel/channel_info_rhs_spec.ts | 4 - e2e-tests/cypress/tests/support/api/role.js | 4 +- .../lib/src/server/default_config.ts | 1 + server/channels/api4/channel.go | 46 +- server/channels/api4/channel_test.go | 506 +++++++++++++++--- server/channels/app/app_test.go | 2 + server/channels/app/channel.go | 38 ++ server/channels/app/channel_test.go | 34 ++ server/channels/app/permissions_migrations.go | 135 +++-- .../localcachelayer/autotranslation_layer.go | 27 - .../channels/store/retrylayer/retrylayer.go | 42 -- .../store/sqlstore/autotranslation_store.go | 56 +- .../channels/store/sqlstore/channel_store.go | 3 +- server/channels/store/store.go | 2 - .../store/storetest/autotranslation.go | 245 +++------ .../storetest/mocks/AutoTranslationStore.go | 46 -- .../channels/store/timerlayer/timerlayer.go | 32 -- server/channels/testlib/store.go | 1 + server/cmd/mmctl/commands/permissions_test.go | 2 + server/config/client.go | 1 + server/einterfaces/autotranslation.go | 4 - .../mocks/AutoTranslationInterface.go | 20 - server/i18n/en.json | 28 + server/public/model/channel.go | 5 + server/public/model/config.go | 5 + server/public/model/migration.go | 1 + server/public/model/permission.go | 16 + server/public/model/post.go | 68 +-- server/public/model/role.go | 4 + .../localization/auto_translation.tsx | 20 + .../permissions_tree.test.tsx.snap | 14 + .../permissions_tree/permissions_tree.tsx | 2 + .../strings/permissions.tsx | 20 + .../channel/details/channel_details.tsx | 3 +- ...hannel_settings_configuration_tab.test.tsx | 2 - .../channel_settings_configuration_tab.tsx | 4 +- .../channel_settings_info_tab.test.tsx | 2 - .../channel_settings_info_tab.tsx | 13 +- webapp/channels/src/i18n/en.json | 6 + .../src/constants/permissions.ts | 2 + webapp/channels/src/utils/constants.tsx | 2 + webapp/platform/types/src/config.ts | 5 + 43 files changed, 919 insertions(+), 604 deletions(-) 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 = {