diff --git a/Makefile b/Makefile index b161c0d1123..7a4d6778f7a 100644 --- a/Makefile +++ b/Makefile @@ -691,6 +691,11 @@ diff-live-manifest: clean-charts charts-integration OUTPUT_FILE="/tmp/wire-server.yaml" ./hack/bin/render-manifest.sh "$(LIVE_VALUES_FILE)"; \ DIFF_OUTPUT_FILE="$(DIFF_OUTPUT_FILE)" ./hack/bin/diff-wire-server-manifests.sh "$(LIVE_MANIFEST_FILE)" /tmp/wire-server.yaml +render-ci-manifest: clean-charts charts-integration + VALUES_FILE="$${VALUES_FILE:-$$(mktemp).yaml}"; \ + ./hack/bin/helm-render-ci-values.sh \ + ./hack/bin/render-manifest.sh "$$VALUES_FILE" + sbom.json: nix -Lv build '.#wireServer.bomDependencies' && \ nix run 'github:wireapp/tom-bombadil#create-sbom' -- --root-package-name "wire-server" diff --git a/changelog.d/5-internal/WPB-23942 b/changelog.d/5-internal/WPB-23942 new file mode 100644 index 00000000000..e1723b0ee08 --- /dev/null +++ b/changelog.d/5-internal/WPB-23942 @@ -0,0 +1 @@ +Consumable notifications are now disabled diff --git a/charts/wire-server/templates/brig/configmap.yaml b/charts/wire-server/templates/brig/configmap.yaml index d5aa494f26c..66435c8d799 100644 --- a/charts/wire-server/templates/brig/configmap.yaml +++ b/charts/wire-server/templates/brig/configmap.yaml @@ -394,5 +394,6 @@ data: {{- if hasKey . "setNomadProfiles" }} setNomadProfiles: {{ index . "setNomadProfiles" }} {{- end }} + setConsumableNotifications: false {{- end }} {{- end }} diff --git a/charts/wire-server/templates/gundeck/configmap.yaml b/charts/wire-server/templates/gundeck/configmap.yaml index 6a107a8e2fd..9f102742700 100644 --- a/charts/wire-server/templates/gundeck/configmap.yaml +++ b/charts/wire-server/templates/gundeck/configmap.yaml @@ -96,5 +96,6 @@ data: {{- if hasKey . "cellsEventQueue" }} cellsEventQueue: {{ .cellsEventQueue }} {{- end }} + consumableNotifications: false {{- end }} diff --git a/docs/src/developer/reference/config-options.md b/docs/src/developer/reference/config-options.md index 911594aac37..2ecd0158d25 100644 --- a/docs/src/developer/reference/config-options.md +++ b/docs/src/developer/reference/config-options.md @@ -2000,6 +2000,23 @@ gundeck: settings: cellsEventQueue: "cells_events" ``` + +## Configure consumable notifications + +The `consumableNotifications` flag controls whether the RabbitMQ-backed Events +API for clients with the `consumable-notifications` capability is operational. +When disabled, the legacy notification flow remains active. + +This is a root-level Helm `values.yaml` setting. It is rendered into both the +`brig` and `gundeck` service configs: + +```yaml +consumableNotifications: false +``` + +- In `brig`, it is rendered as `optSettings.setConsumableNotifications`. +- In `gundeck`, it is rendered as `settings.consumableNotifications`. + ## Background worker: Background jobs The background worker processes asynchronous jobs (conversation migrations, backend notifications). Configuration is supplied via Helm under `background-worker.config` and rendered into `background-worker.yaml`. diff --git a/hack/bin/helm-render-ci-values.sh b/hack/bin/helm-render-ci-values.sh new file mode 100755 index 00000000000..a1f3a6babc2 --- /dev/null +++ b/hack/bin/helm-render-ci-values.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# best run via make render-wire-server-resources +# otherwise run make clean-charts and make charts-integration before + +set -euo pipefail + +VALUES_FILE="${VALUES_FILE:-$(mktemp).yaml}" + +# from repo root +export NAMESPACE_1="${NAMESPACE_1:-test-a}" +export NAMESPACE_2="${NAMESPACE_2:-test-b}" +export FEDERATION_DOMAIN_1="${FEDERATION_DOMAIN_1:-integration.example.com}" +export FEDERATION_DOMAIN_2="${FEDERATION_DOMAIN_2:-integration2.example.com}" +export FEDERATION_DOMAIN_BASE_1="${FEDERATION_DOMAIN_BASE_1:-example.com}" +export FEDERATION_DOMAIN_BASE_2="${FEDERATION_DOMAIN_BASE_2:-example.com}" +export FEDERATION_CA_CERTIFICATE="${FEDERATION_CA_CERTIFICATE:-$(cat services/nginz/integration-test/conf/nginz/integration-ca.pem)}" +export ENTERPRISE_IMAGE_PULL_SECRET="${ENTERPRISE_IMAGE_PULL_SECRET:-{}}" + +helmfile -f hack/helmfile.yaml.gotmpl \ + -e default \ + --skip-deps \ + -l name=wire-server,namespace="${NAMESPACE_1}" \ + write-values \ + --output-file-template "${VALUES_FILE}" + +echo "Rendered values: $VALUES_FILE" diff --git a/hack/helm_vars/wire-server/values.yaml.gotmpl b/hack/helm_vars/wire-server/values.yaml.gotmpl index 88386340f99..5255592075c 100644 --- a/hack/helm_vars/wire-server/values.yaml.gotmpl +++ b/hack/helm_vars/wire-server/values.yaml.gotmpl @@ -6,6 +6,8 @@ tags: sftd: false integration: true +consumableNotifications: false + cassandra-migrations: imagePullPolicy: {{ .Values.imagePullPolicy }} cassandra: diff --git a/integration/test/Test/Events.hs b/integration/test/Test/Events.hs index d85b42ba326..a172cf2ff0f 100644 --- a/integration/test/Test/Events.hs +++ b/integration/test/Test/Events.hs @@ -55,8 +55,8 @@ import Testlib.ResourcePool import UnliftIO hiding (handle) testConsumeEventsOneWebSocket :: (HasCallStack) => App () -testConsumeEventsOneWebSocket = do - alice <- randomUser OwnDomain def +testConsumeEventsOneWebSocket = withModifiedBackend (enableConsumableNotifications def) $ \domain -> do + alice <- randomUser domain def lastNotifResp <- retrying @@ -95,11 +95,13 @@ testConsumeEventsOneWebSocket = do testWebSocketTimeout :: (HasCallStack) => App () testWebSocketTimeout = withModifiedBackend - def - { cannonCfg = - setField "wsOpts.activityTimeout" (1000000 :: Int) - >=> setField "wsOpts.pongTimeout" (1000000 :: Int) - } + ( enableConsumableNotifications + def + { cannonCfg = + setField "wsOpts.activityTimeout" (1000000 :: Int) + >=> setField "wsOpts.pongTimeout" (1000000 :: Int) + } + ) $ \domain -> do alice <- randomUser domain def client <- addClient alice def {acapabilities = Just ["consumable-notifications"]} >>= getJSON 201 @@ -118,8 +120,8 @@ testWebSocketTimeout = withModifiedBackend $ assertFailure "Expected web socket timeout" testConsumeTempEvents :: (HasCallStack) => App () -testConsumeTempEvents = do - alice <- randomUser OwnDomain def +testConsumeTempEvents = withModifiedBackend (enableConsumableNotifications def) $ \domain -> do + alice <- randomUser domain def client0 <- addClient alice def {acapabilities = Just ["consumable-notifications"]} >>= getJSON 201 clientId0 <- objId client0 @@ -160,7 +162,7 @@ testConsumeTempEvents = do testTemporaryQueuesAreDeletedAfterUse :: (HasCallStack) => App () testTemporaryQueuesAreDeletedAfterUse = do - startDynamicBackendsReturnResources [def] $ \[beResource] -> do + startDynamicBackendsReturnResources [enableConsumableNotifications def] $ \[beResource] -> do let domain = beResource.berDomain rabbitmqAdmin <- mkRabbitMqAdminClientForResource beResource [alice, bob] <- createAndConnectUsers [domain, domain] @@ -204,8 +206,8 @@ testTemporaryQueuesAreDeletedAfterUse = do queuesAfterWS.items `shouldMatchSet` [deadNotifsQueue, cellsEventsQueue, aliceClientQueue, backgroundJobsQueue] testSendMessageNoReturnToSenderWithConsumableNotificationsProteus :: (HasCallStack) => App () -testSendMessageNoReturnToSenderWithConsumableNotificationsProteus = do - (alice, tid, bob : _) <- createTeam OwnDomain 2 +testSendMessageNoReturnToSenderWithConsumableNotificationsProteus = withModifiedBackend (enableConsumableNotifications def) $ \domain -> do + (alice, tid, bob : _) <- createTeam domain 2 aliceOldClient <- addClient alice def >>= getJSON 201 aliceClient <- addClient alice def {acapabilities = Just ["consumable-notifications"]} >>= getJSON 201 aliceClientId <- objId aliceClient @@ -237,8 +239,8 @@ testSendMessageNoReturnToSenderWithConsumableNotificationsProteus = do assertNoEvent_ ws testEventsForSpecificClients :: (HasCallStack) => App () -testEventsForSpecificClients = do - alice <- randomUser OwnDomain def +testEventsForSpecificClients = withModifiedBackend (enableConsumableNotifications def) $ \domain -> do + alice <- randomUser domain def uid <- objId alice client1 <- addClient alice def {acapabilities = Just ["consumable-notifications"]} >>= getJSON 201 cid1 <- objId client1 @@ -262,7 +264,7 @@ testEventsForSpecificClients = do "payload" .= [object ["hello" .= "client2"]] ] - GundeckInternal.postPush OwnDomain [eventForClient1, eventForClient2] >>= assertSuccess + GundeckInternal.postPush domain [eventForClient1, eventForClient2] >>= assertSuccess assertEvent ws1 $ \e -> e %. "data.event.payload.0.hello" `shouldMatch` "client1" @@ -273,9 +275,9 @@ testEventsForSpecificClients = do $ assertNoEvent_ wsTemp testConsumeEventsForDifferentUsers :: (HasCallStack) => App () -testConsumeEventsForDifferentUsers = do - alice <- randomUser OwnDomain def - bob <- randomUser OwnDomain def +testConsumeEventsForDifferentUsers = withModifiedBackend (enableConsumableNotifications def) $ \domain -> do + alice <- randomUser domain def + bob <- randomUser domain def aliceClient <- addClient alice def {acapabilities = Just ["consumable-notifications"]} >>= getJSON 201 aliceClientId <- objId aliceClient @@ -299,8 +301,8 @@ testConsumeEventsForDifferentUsers = do sendAck ws deliveryTag False testConsumeEventsWhileHavingLegacyClients :: (HasCallStack) => App () -testConsumeEventsWhileHavingLegacyClients = do - alice <- randomUser OwnDomain def +testConsumeEventsWhileHavingLegacyClients = withModifiedBackend (enableConsumableNotifications def) $ \domain -> do + alice <- randomUser domain def -- Even if alice has no clients, the notifications should still be persisted -- in Cassandra. This choice is kinda arbitrary as these notifications @@ -333,8 +335,8 @@ testConsumeEventsWhileHavingLegacyClients = do resp.json %. "notifications.1.payload.0.type" `shouldMatch` "user.client-add" testConsumeEventsAcks :: (HasCallStack) => App () -testConsumeEventsAcks = do - alice <- randomUser OwnDomain def +testConsumeEventsAcks = withModifiedBackend (enableConsumableNotifications def) $ \domain -> do + alice <- randomUser domain def client <- addClient alice def {acapabilities = Just ["consumable-notifications"]} >>= getJSON 201 clientId <- objId client @@ -355,8 +357,8 @@ testConsumeEventsAcks = do assertNoEvent_ ws testConsumeEventsMultipleAcks :: (HasCallStack) => App () -testConsumeEventsMultipleAcks = do - alice <- randomUser OwnDomain def +testConsumeEventsMultipleAcks = withModifiedBackend (enableConsumableNotifications def) $ \domain -> do + alice <- randomUser domain def client <- addClient alice def {acapabilities = Just ["consumable-notifications"]} >>= getJSON 201 clientId <- objId client @@ -379,8 +381,8 @@ testConsumeEventsMultipleAcks = do assertNoEvent_ ws testConsumeEventsAckNewEventWithoutAckingOldOne :: (HasCallStack) => App () -testConsumeEventsAckNewEventWithoutAckingOldOne = do - alice <- randomUser OwnDomain def +testConsumeEventsAckNewEventWithoutAckingOldOne = withModifiedBackend (enableConsumableNotifications def) $ \domain -> do + alice <- randomUser domain def client <- addClient alice def {acapabilities = Just ["consumable-notifications"]} >>= getJSON 201 clientId <- objId client @@ -415,7 +417,7 @@ testConsumeEventsAckNewEventWithoutAckingOldOne = do testEventsDeadLettered :: (HasCallStack) => App () testEventsDeadLettered = do let notifTTL = 1 # Second - withModifiedBackend (def {gundeckCfg = setField "settings.notificationTTL" (notifTTL #> Second)}) $ \domain -> do + withModifiedBackend (enableConsumableNotifications (def {gundeckCfg = setField "settings.notificationTTL" (notifTTL #> Second)})) $ \domain -> do alice <- randomUser domain def -- This generates an event @@ -449,7 +451,7 @@ testEventsDeadLettered = do testEventsDeadLetteredWithReconnect :: (HasCallStack) => App () testEventsDeadLetteredWithReconnect = do let notifTTL = 1 # Second - startDynamicBackendsReturnResources [def {gundeckCfg = setField "settings.notificationTTL" (notifTTL #> Second)}] $ \[resources] -> do + startDynamicBackendsReturnResources [enableConsumableNotifications (def {gundeckCfg = setField "settings.notificationTTL" (notifTTL #> Second)})] $ \[resources] -> do let domain :: String = resources.berDomain alice <- randomUser domain def @@ -501,7 +503,7 @@ testEventsDeadLetteredWithReconnect = do testTransientEventsDoNotTriggerDeadLetters :: (HasCallStack) => App () testTransientEventsDoNotTriggerDeadLetters = do let notifTTL = 1 # Second - withModifiedBackend (def {gundeckCfg = setField "settings.notificationTTL" (notifTTL #> Second)}) $ \domain -> do + withModifiedBackend (enableConsumableNotifications (def {gundeckCfg = setField "settings.notificationTTL" (notifTTL #> Second)})) $ \domain -> do alice <- randomUser domain def -- Creates a non-transient event client <- addClient alice def {acapabilities = Just ["consumable-notifications"]} >>= getJSON 201 @@ -527,9 +529,9 @@ testTransientEventsDoNotTriggerDeadLetters = do assertNoEvent_ ws testTransientEvents :: (HasCallStack) => App () -testTransientEvents = do - (alice, _, _) <- mkUserPlusClient - (bob, _, bobClient) <- mkUserPlusClient +testTransientEvents = withModifiedBackend (enableConsumableNotifications def) $ \domain -> do + (alice, _, _) <- mkUserPlusClientWithDomain domain + (bob, _, bobClient) <- mkUserPlusClientWithDomain domain connectTwoUsers alice bob bobClientId <- objId bobClient @@ -567,11 +569,13 @@ testTransientEvents = do testChannelLimit :: (HasCallStack) => App () testChannelLimit = withModifiedBackend - ( def - { cannonCfg = - setField "rabbitMqMaxChannels" (2 :: Int) - >=> setField "rabbitMqMaxConnections" (1 :: Int) - } + ( enableConsumableNotifications + ( def + { cannonCfg = + setField "rabbitMqMaxChannels" (2 :: Int) + >=> setField "rabbitMqMaxConnections" (1 :: Int) + } + ) ) $ \domain -> do alice <- randomUser domain def @@ -609,7 +613,7 @@ testChannelKilled = do void $ killAllRabbitMqConns backend waitUntilNoRabbitMqConns backend - runCodensity (startDynamicBackend backend def) $ \_ -> do + runCodensity (startDynamicBackend backend (enableConsumableNotifications def)) $ \_ -> do let domain = backend.berDomain alice <- randomUser domain def [c1, c2] <- @@ -643,8 +647,8 @@ testChannelKilled = do assertNoEventHelper ws `shouldMatch` WebSocketDied testSingleConsumer :: (HasCallStack) => App () -testSingleConsumer = do - alice <- randomUser OwnDomain def +testSingleConsumer = withModifiedBackend (enableConsumableNotifications def) $ \domain -> do + alice <- randomUser domain def clientId <- addClient alice def {acapabilities = Just ["consumable-notifications"]} >>= getJSON 201 @@ -682,8 +686,8 @@ testSingleConsumer = do lift $ assertNoEvent_ ws' testPrefetchCount :: (HasCallStack) => App () -testPrefetchCount = do - (alice, uid, cid) <- mkUserPlusClient +testPrefetchCount = withModifiedBackend (enableConsumableNotifications def) $ \domain -> do + (alice, uid, cid) <- mkUserPlusClientWithDomain domain emptyQueue alice cid for_ [1 :: Int .. 550] $ \i -> @@ -693,7 +697,7 @@ testPrefetchCount = do [ "recipients" .= [object ["user_id" .= uid, "clients" .= [cid], "route" .= "any"]], "payload" .= [object ["no" .= show i]] ] - GundeckInternal.postPush OwnDomain [event] >>= assertSuccess + GundeckInternal.postPush domain [event] >>= assertSuccess runCodensity (createEventsWebSocketWithSync alice (Just cid)) \(endMarker, ws) -> do es <- consumeAllEventsNoAck ws assertBool ("First 500 events expected, got " ++ show (length es)) $ length es == 500 @@ -704,11 +708,11 @@ testPrefetchCount = do assertBool "Receive at least one outstanding event" $ not (null es') testEndOfInitialSync :: (HasCallStack) => App () -testEndOfInitialSync = do - (alice, uid, cid) <- mkUserPlusClient +testEndOfInitialSync = withModifiedBackend (enableConsumableNotifications def) $ \domain -> do + (alice, uid, cid) <- mkUserPlusClientWithDomain domain let n = 20 replicateM_ n $ do - GundeckInternal.postPush OwnDomain [mkEvent uid cid False] >>= assertSuccess + GundeckInternal.postPush domain [mkEvent uid cid False] >>= assertSuccess -- marker0 <- randomId runCodensity (createEventsWebSocketWithSync alice (Just cid)) \(endMarker, ws) -> do @@ -719,7 +723,7 @@ testEndOfInitialSync = do length (preExistingEvents <> otherEvents) `shouldMatchInt` (n + 2) -- more events should not be followed by the sync event - GundeckInternal.postPush OwnDomain [mkEvent uid cid False] >>= assertSuccess + GundeckInternal.postPush domain [mkEvent uid cid False] >>= assertSuccess assertEvent ws $ \e -> do e %. "data.event.payload.0.type" `shouldMatch` "test" ackEvent ws e @@ -734,18 +738,18 @@ testEndOfInitialSync = do length events `shouldMatchInt` 1 -- more events should not be followed by synchronization event - GundeckInternal.postPush OwnDomain [mkEvent uid cid False] >>= assertSuccess + GundeckInternal.postPush domain [mkEvent uid cid False] >>= assertSuccess assertEvent ws $ \e -> do e %. "data.event.payload.0.type" `shouldMatch` "test" ackEvent ws e assertNoEvent_ ws testEndOfInitialSyncMoreEventsAfterSyncMessage :: (HasCallStack) => App () -testEndOfInitialSyncMoreEventsAfterSyncMessage = do - (alice, uid, cid) <- mkUserPlusClient +testEndOfInitialSyncMoreEventsAfterSyncMessage = withModifiedBackend (enableConsumableNotifications def) $ \domain -> do + (alice, uid, cid) <- mkUserPlusClientWithDomain domain let n = 20 replicateM_ n $ do - GundeckInternal.postPush OwnDomain [mkEvent uid cid False] >>= assertSuccess + GundeckInternal.postPush domain [mkEvent uid cid False] >>= assertSuccess runCodensity (createEventsWebSocketWithSync alice (Just cid)) \(endMarker, ws) -> do -- it seems this is needed to reduce flakiness, @@ -754,7 +758,7 @@ testEndOfInitialSyncMoreEventsAfterSyncMessage = do -- before consuming, we push n more events replicateM_ n $ do - GundeckInternal.postPush OwnDomain [mkEvent uid cid False] >>= assertSuccess + GundeckInternal.postPush domain [mkEvent uid cid False] >>= assertSuccess preExistingEvents <- consumeEventsUntilEndOfInitialSync ws endMarker otherEvents <- consumeAllEvents ws @@ -765,14 +769,14 @@ testEndOfInitialSyncMoreEventsAfterSyncMessage = do `shouldMatch` True testEndOfInitialSyncIgnoreExpired :: (HasCallStack) => App () -testEndOfInitialSyncIgnoreExpired = do - (alice, uid, cid) <- mkUserPlusClient +testEndOfInitialSyncIgnoreExpired = withModifiedBackend (enableConsumableNotifications def) $ \domain -> do + (alice, uid, cid) <- mkUserPlusClientWithDomain domain let n = 20 replicateM_ n $ do - GundeckInternal.postPush OwnDomain [mkEvent uid cid False] >>= assertSuccess + GundeckInternal.postPush domain [mkEvent uid cid False] >>= assertSuccess replicateM_ n $ do - GundeckInternal.postPush OwnDomain [mkEvent uid cid True] >>= assertSuccess + GundeckInternal.postPush domain [mkEvent uid cid True] >>= assertSuccess -- Wait for transient events to expire Timeout.threadDelay (1 # Second) @@ -784,11 +788,11 @@ testEndOfInitialSyncIgnoreExpired = do length events `shouldMatchInt` (n + 2) -- +1 for the sync event, +1 for the client add event testEndOfInitialSyncAckMultiple :: (HasCallStack) => App () -testEndOfInitialSyncAckMultiple = do - (alice, uid, cid) <- mkUserPlusClient +testEndOfInitialSyncAckMultiple = withModifiedBackend (enableConsumableNotifications def) $ \domain -> do + (alice, uid, cid) <- mkUserPlusClientWithDomain domain let n = 20 replicateM_ n $ do - GundeckInternal.postPush OwnDomain [mkEvent uid cid False] >>= assertSuccess + GundeckInternal.postPush domain [mkEvent uid cid False] >>= assertSuccess runCodensity (createEventsWebSocketWithSync alice (Just cid)) $ \(endMarker, ws) -> do void $ assertEvent ws pure @@ -812,43 +816,51 @@ mkEvent uid cid transient = testTypingIndicatorIsNotSentToOwnClient :: (HasCallStack) => TaggedBool "federated" -> App () testTypingIndicatorIsNotSentToOwnClient (TaggedBool federated) = do - (alice, _, aliceClient) <- mkUserPlusClientWithDomain OwnDomain - (bob, _, bobClient) <- mkUserPlusClientWithDomain (if federated then OtherDomain else OwnDomain) - connectTwoUsers alice bob - aliceClientId <- objId aliceClient - bobClientId <- objId bobClient - conv <- postConversation alice defProteus {qualifiedUsers = [bob]} >>= getJSON 201 - - runCodensity (createEventWebSockets [(alice, Just aliceClientId), (bob, Just bobClientId)]) $ \[aliceWs, bobWs] -> do - -- consume all events to ensure we start with a clean slate - consumeAllEvents_ aliceWs - consumeAllEvents_ bobWs - - -- Alice is typing - sendTypingStatus alice conv "started" >>= assertSuccess - - -- Bob should receive the typing indicator for Alice - assertEvent bobWs $ \e -> do - e %. "data.event.payload.0.type" `shouldMatch` "conversation.typing" - e %. "data.event.payload.0.qualified_conversation" `shouldMatch` (conv %. "qualified_id") - e %. "data.event.payload.0.qualified_from" `shouldMatch` (alice %. "qualified_id") - ackEvent bobWs e - - -- Alice should not receive the typing indicator for herself - assertNoEvent_ aliceWs - - -- Bob is typing - sendTypingStatus bob conv "started" >>= assertSuccess - - -- Alice should receive the typing indicator for Bob - assertEvent aliceWs $ \e -> do - e %. "data.event.payload.0.type" `shouldMatch` "conversation.typing" - e %. "data.event.payload.0.qualified_conversation" `shouldMatch` (conv %. "qualified_id") - e %. "data.event.payload.0.qualified_from" `shouldMatch` (bob %. "qualified_id") - ackEvent aliceWs e - - -- Bob should not receive the typing indicator for himself - assertNoEvent_ bobWs + let runTest = + if federated + then startDynamicBackends [(enableConsumableNotifications def), (enableConsumableNotifications def)] + else \run -> startDynamicBackends [(enableConsumableNotifications def)] $ \[domain] -> run [domain, domain] + runTest $ \[domain, otherDomain] -> do + (alice, _, aliceClient) <- mkUserPlusClientWithDomain domain + (bob, _, bobClient) <- mkUserPlusClientWithDomain otherDomain + connectTwoUsers alice bob + aliceClientId <- objId aliceClient + bobClientId <- objId bobClient + conv <- postConversation alice defProteus {qualifiedUsers = [bob]} >>= getJSON 201 + + runCodensity (createEventWebSockets [(alice, Just aliceClientId), (bob, Just bobClientId)]) $ \[aliceWs, bobWs] -> do + -- consume all events to ensure we start with a clean slate + consumeAllEvents_ aliceWs + consumeAllEvents_ bobWs + + -- Alice is typing + sendTypingStatus alice conv "started" >>= assertSuccess + + -- Bob should receive the typing indicator for Alice + assertEvent bobWs $ \e -> do + e %. "data.event.payload.0.type" `shouldMatch` "conversation.typing" + e %. "data.event.payload.0.qualified_conversation" `shouldMatch` (conv %. "qualified_id") + e %. "data.event.payload.0.qualified_from" `shouldMatch` (alice %. "qualified_id") + ackEvent bobWs e + + -- Alice should not receive the typing indicator for herself + assertNoEvent_ aliceWs + + -- Bob is typing + sendTypingStatus bob conv "started" >>= assertSuccess + + -- Alice should receive the typing indicator for Bob + assertEvent aliceWs $ \e -> do + e %. "data.event.payload.0.type" `shouldMatch` "conversation.typing" + e %. "data.event.payload.0.qualified_conversation" `shouldMatch` (conv %. "qualified_id") + e %. "data.event.payload.0.qualified_from" `shouldMatch` (bob %. "qualified_id") + ackEvent aliceWs e + + -- Bob should not receive the typing indicator for himself + assertNoEvent_ bobWs + +-- convert :: ((HasCallStack) => (String -> App ()) -> App ()) -> ([String] -> App ()) -> App () +-- convert = undefined -- We only delete queues to clean up federated integration tests. So, we -- mostly want to ensure we don't get stuck there. @@ -860,12 +872,14 @@ testBackendPusherRecoversFromQueueDeletion = do let remotesRefreshInterval = 10000 :: Int startDynamicBackendsReturnResources - [ def - { backgroundWorkerCfg = - setField - "backendNotificationPusher.remotesRefreshInterval" - remotesRefreshInterval - } + [ enableConsumableNotifications + ( def + { backgroundWorkerCfg = + setField + "backendNotificationPusher.remotesRefreshInterval" + remotesRefreshInterval + } + ) ] $ \[beResource] -> do let domain = beResource.berDomain @@ -1243,3 +1257,11 @@ mkRabbitMqAdminClientForResource backend = do opts <- asks (.rabbitMQConfig) servantClient <- liftIO $ mkRabbitMqAdminClientEnv opts {vHost = Text.pack backend.berVHost} pure . fromServant $ Servant.hoistClient (Proxy @(ToServant AdminAPI AsApi)) (liftIO @App) (toServant servantClient) + +enableConsumableNotifications :: ServiceOverrides -> ServiceOverrides +enableConsumableNotifications overrides = + overrides + <> def + { brigCfg = setField "optSettings.setConsumableNotifications" True, + gundeckCfg = setField "settings.consumableNotifications" True + } diff --git a/services/brig/brig.integration.yaml b/services/brig/brig.integration.yaml index 7d39b7166de..fc23b069f74 100644 --- a/services/brig/brig.integration.yaml +++ b/services/brig/brig.integration.yaml @@ -267,6 +267,7 @@ optSettings: maxRateLimitedKeys: 100000 # Estimated memory usage: 4 MB setChallengeTTL: 172800 setEphemeralUserCreationEnabled: true + setConsumableNotifications: false logLevel: Warn logNetStrings: false diff --git a/services/brig/src/Brig/API/Client.hs b/services/brig/src/Brig/API/Client.hs index 89a0fc641f1..ce43b65844d 100644 --- a/services/brig/src/Brig/API/Client.hs +++ b/services/brig/src/Brig/API/Client.hs @@ -218,7 +218,8 @@ addClientWithReAuthPolicy policy luid@(tUnqualified -> u) con new = do (Data.addClientWithReAuthPolicy policy luid clientId' new maxPermClients mCaps) !>> ClientDataError let clt = clt0 {clientMLSPublicKeys = newClientMLSPublicKeys new} - when (supportsConsumableNotifications clt) $ lift $ liftSem $ do + consumableNotificationsEnabled <- asks (.settings.consumableNotifications) + when (consumableNotificationsEnabled && supportsConsumableNotifications clt) $ lift $ liftSem $ do setupConsumableNotifications u clt.clientId lift $ do for_ old $ execDelete u con @@ -253,13 +254,14 @@ updateClient :: (Handler r) () updateClient uid cid req = do client <- (lift (liftSem (ClientStore.lookupClient uid cid)) >>= maybe (throwE ClientNotFound) pure) !>> clientError + consumableNotificationsEnabled <- asks (.settings.consumableNotifications) lift . liftSem $ for_ req.updateClientLabel $ ClientStore.updateLabel uid cid . Just for_ req.updateClientCapabilities $ \caps -> do if client.clientCapabilities.fromClientCapabilityList `Set.isSubsetOf` caps.fromClientCapabilityList then do -- first set up the notification queues then save the data is more robust than the other way around let addedCapabilities = caps.fromClientCapabilityList \\ client.clientCapabilities.fromClientCapabilityList - when (ClientSupportsConsumableNotifications `Set.member` addedCapabilities) $ lift $ liftSem $ do + when (consumableNotificationsEnabled && ClientSupportsConsumableNotifications `Set.member` addedCapabilities) $ lift $ liftSem $ do setupConsumableNotifications uid cid lift . liftSem . ClientStore.updateCapabilities uid cid . Just $ caps else throwE $ clientError ClientCapabilitiesCannotBeRemoved diff --git a/services/brig/src/Brig/Options.hs b/services/brig/src/Brig/Options.hs index 47122739bd2..91cf542e487 100644 --- a/services/brig/src/Brig/Options.hs +++ b/services/brig/src/Brig/Options.hs @@ -591,7 +591,9 @@ data Settings = Settings -- | Whether to allow ephemeral user creation ephemeralUserCreationEnabled :: !Bool, -- | Determines if this backend supports nomad profiles. - nomadProfiles :: !(Maybe Bool) + nomadProfiles :: !(Maybe Bool), + -- | Determines if consumable notifications are enabled + consumableNotifications :: !Bool } deriving (Show, Generic) diff --git a/services/gundeck/gundeck.integration.yaml b/services/gundeck/gundeck.integration.yaml index 1c33557402a..00c80574794 100644 --- a/services/gundeck/gundeck.integration.yaml +++ b/services/gundeck/gundeck.integration.yaml @@ -55,6 +55,7 @@ settings: # brig, cannon, cargohold, galley, gundeck, proxy, spar. disabledAPIVersions: [] cellsEventQueue: "cells_events" + consumableNotifications: false logLevel: Warn logNetStrings: false diff --git a/services/gundeck/src/Gundeck/API/Internal.hs b/services/gundeck/src/Gundeck/API/Internal.hs index c1c1591ab8d..296b8f4f87e 100644 --- a/services/gundeck/src/Gundeck/API/Internal.hs +++ b/services/gundeck/src/Gundeck/API/Internal.hs @@ -22,11 +22,12 @@ module Gundeck.API.Internal where import Cassandra qualified -import Control.Lens (view) +import Control.Lens (view, (^.)) import Data.Id import Gundeck.Client import Gundeck.Client qualified as Client import Gundeck.Monad +import Gundeck.Options (consumableNotifications, settings) import Gundeck.Presence qualified as Presence import Gundeck.Push qualified as Push import Gundeck.Push.Data qualified as PushTok @@ -69,6 +70,8 @@ getPushTokensH uid = PushTok.PushTokenList <$> (view PushTok.addrPushToken <$$> registerConsumableNotificationsClient :: UserId -> ClientId -> Gundeck NoContent registerConsumableNotificationsClient uid cid = do - chan <- getRabbitMqChan - void . liftIO $ setupConsumableNotifications chan uid cid + enabled <- asks (^. options . settings . consumableNotifications) + when enabled $ do + chan <- getRabbitMqChan + void . liftIO $ setupConsumableNotifications chan uid cid pure NoContent diff --git a/services/gundeck/src/Gundeck/Options.hs b/services/gundeck/src/Gundeck/Options.hs index ee55c98bebe..d70bbc4f91d 100644 --- a/services/gundeck/src/Gundeck/Options.hs +++ b/services/gundeck/src/Gundeck/Options.hs @@ -84,7 +84,9 @@ data Settings = Settings -- notifications from the database if notifications have inlined payloads. _internalPageSize :: Maybe Int32, -- | The name of the RabbitMQ queue to be used to forward events to Cells. - _cellsEventQueue :: !(Maybe Text) + _cellsEventQueue :: !(Maybe Text), + -- | Determines if consumable notifications are enabled + _consumableNotifications :: !Bool } deriving (Show, Generic) diff --git a/services/gundeck/src/Gundeck/Push.hs b/services/gundeck/src/Gundeck/Push.hs index 3609bb5dfb0..9a7e8d5295c 100644 --- a/services/gundeck/src/Gundeck/Push.hs +++ b/services/gundeck/src/Gundeck/Push.hs @@ -104,6 +104,7 @@ push ps = do -- | Abstract over all effects in 'pushAll' (for unit testing). class (MonadThrow m) => MonadPushAll m where + mpaConsumableNotificationsEnabled :: m Bool mpaNotificationTTL :: m NotificationTTL mpaCellsEventQueue :: m (Maybe Text) mpaMkNotificationId :: m NotificationId @@ -117,6 +118,7 @@ class (MonadThrow m) => MonadPushAll m where mpaPublishToRabbitMq :: Text -> Text -> Q.Message -> m () instance MonadPushAll Gundeck where + mpaConsumableNotificationsEnabled = view (options . settings . consumableNotifications) mpaNotificationTTL = view (options . settings . notificationTTL) mpaCellsEventQueue = view (options . settings . cellsEventQueue) mpaMkNotificationId = mkNotificationId @@ -241,13 +243,19 @@ getClients uids = do pushAll :: (MonadPushAll m, MonadNativeTargets m, MonadMapAsync m, Log.MonadLogger m) => [Push] -> m () pushAll pushes = do Log.debug $ msg (val "pushing") . Log.field "pushes" (Aeson.encode pushes) - (rabbitmqPushes, legacyPushes, allUserClients) <- splitPushes pushes + consumableNotificationsEnabled <- mpaConsumableNotificationsEnabled + (rabbitmqPushes, legacyPushes, allUserClients) <- + if consumableNotificationsEnabled + then splitPushes pushes + else do + allUserClients <- mpaGetClients (Set.unions $ map (\p -> Set.map (._recipientId) $ p._pushRecipients) pushes) + pure ([], pushes, allUserClients) legacyNotifs <- mapM mkNewNotification legacyPushes pushAllLegacy legacyNotifs allUserClients rabbitmqNotifs <- mapM mkNewNotification rabbitmqPushes - pushAllViaRabbitMq rabbitmqNotifs allUserClients + unless (null rabbitmqNotifs) $ pushAllViaRabbitMq rabbitmqNotifs allUserClients -- Note that Cells needs all notifications because it doesn't matter whether -- some recipients have rabbitmq clients or not. diff --git a/services/gundeck/test/unit/MockGundeck.hs b/services/gundeck/test/unit/MockGundeck.hs index cb7b4f5fa88..f6d6ebe44f0 100644 --- a/services/gundeck/test/unit/MockGundeck.hs +++ b/services/gundeck/test/unit/MockGundeck.hs @@ -433,6 +433,7 @@ instance MonadThrow MockGundeck where -- as well crash badly here, as long as it doesn't go unnoticed...) instance MonadPushAll MockGundeck where + mpaConsumableNotificationsEnabled = pure True mpaNotificationTTL = pure $ NotificationTTL 300 -- (longer than we want any test to take.) mpaCellsEventQueue = pure $ Just "cells" mpaMkNotificationId = mockMkNotificationId