diff --git a/cabal.project b/cabal.project index d490134c847..1fcef790a84 100644 --- a/cabal.project +++ b/cabal.project @@ -48,6 +48,7 @@ packages: , tools/db/migrate-sso-feature-flag/ , tools/db/move-team/ , tools/db/phone-users/ + , tools/db/conference-feature/ , tools/db/repair-handles/ , tools/db/repair-brig-clients-table/ , tools/db/service-backfill/ diff --git a/nix/local-haskell-packages.nix b/nix/local-haskell-packages.nix index 133fcd9afae..9261469770f 100644 --- a/nix/local-haskell-packages.nix +++ b/nix/local-haskell-packages.nix @@ -44,6 +44,7 @@ spar = hself.callPackage ../services/spar/default.nix { inherit gitignoreSource; }; assets = hself.callPackage ../tools/db/assets/default.nix { inherit gitignoreSource; }; auto-whitelist = hself.callPackage ../tools/db/auto-whitelist/default.nix { inherit gitignoreSource; }; + conference-feature = hself.callPackage ../tools/db/conference-feature/default.nix { inherit gitignoreSource; }; find-undead = hself.callPackage ../tools/db/find-undead/default.nix { inherit gitignoreSource; }; inconsistencies = hself.callPackage ../tools/db/inconsistencies/default.nix { inherit gitignoreSource; }; migrate-sso-feature-flag = hself.callPackage ../tools/db/migrate-sso-feature-flag/default.nix { inherit gitignoreSource; }; diff --git a/nix/wire-server.nix b/nix/wire-server.nix index 9f3eed3d4e4..ec23324f556 100644 --- a/nix/wire-server.nix +++ b/nix/wire-server.nix @@ -86,6 +86,7 @@ let integration = [ "integration" ]; rabbitmq-consumer = [ "rabbitmq-consumer" ]; test-stats = [ "test-stats" ]; + conference-feature = [ "conference-feature" ]; }; inherit (lib) attrsets; diff --git a/tools/db/conference-feature/.ormolu b/tools/db/conference-feature/.ormolu new file mode 120000 index 00000000000..ffc2ca9745e --- /dev/null +++ b/tools/db/conference-feature/.ormolu @@ -0,0 +1 @@ +../../../.ormolu \ No newline at end of file diff --git a/tools/db/conference-feature/app/Main.hs b/tools/db/conference-feature/app/Main.hs new file mode 100644 index 00000000000..93811d3a4a2 --- /dev/null +++ b/tools/db/conference-feature/app/Main.hs @@ -0,0 +1,23 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2024 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Main where + +import qualified ConferenceFeature.Lib as Lib + +main :: IO () +main = Lib.main diff --git a/tools/db/conference-feature/conference-feature.cabal b/tools/db/conference-feature/conference-feature.cabal new file mode 100644 index 00000000000..ae22e5a3488 --- /dev/null +++ b/tools/db/conference-feature/conference-feature.cabal @@ -0,0 +1,98 @@ +cabal-version: 3.0 +name: conference-feature +version: 1.0.0 +synopsis: check the user's conference feature +category: Network +author: Wire Swiss GmbH +maintainer: Wire Swiss GmbH +copyright: (c) 2024 Wire Swiss GmbH +license: AGPL-3.0-only +build-type: Simple + +library + hs-source-dirs: src + exposed-modules: + ConferenceFeature.Lib + ConferenceFeature.Types + + ghc-options: + -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates + -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path + -funbox-strict-fields -threaded -with-rtsopts=-N + -Wredundant-constraints -Wunused-packages + + build-depends: + , aeson + , aeson-pretty + , base + , bytestring + , cassandra-util + , conduit + , containers + , cql + , exceptions + , imports + , lens + , optparse-applicative + , tinylog + , types-common + , wire-api + + default-extensions: + AllowAmbiguousTypes + BangPatterns + ConstraintKinds + DataKinds + DefaultSignatures + DeriveFunctor + DeriveGeneric + DeriveLift + DeriveTraversable + DerivingStrategies + DerivingVia + DuplicateRecordFields + EmptyCase + FlexibleContexts + FlexibleInstances + FunctionalDependencies + GADTs + GeneralizedNewtypeDeriving + InstanceSigs + KindSignatures + LambdaCase + MultiParamTypeClasses + MultiWayIf + NamedFieldPuns + NoImplicitPrelude + OverloadedLabels + OverloadedRecordDot + OverloadedStrings + PackageImports + PatternSynonyms + PolyKinds + QuasiQuotes + RankNTypes + RecordWildCards + ScopedTypeVariables + StandaloneDeriving + TupleSections + TypeApplications + TypeFamilies + TypeFamilyDependencies + TypeOperators + UndecidableInstances + ViewPatterns + +executable conference-feature + main-is: Main.hs + build-depends: + , base + , conference-feature + + hs-source-dirs: app + default-language: Haskell2010 + ghc-options: + -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates + -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path + -funbox-strict-fields -threaded -with-rtsopts=-N + -Wredundant-constraints -Wunused-packages diff --git a/tools/db/conference-feature/default.nix b/tools/db/conference-feature/default.nix new file mode 100644 index 00000000000..e415d45c691 --- /dev/null +++ b/tools/db/conference-feature/default.nix @@ -0,0 +1,53 @@ +# WARNING: GENERATED FILE, DO NOT EDIT. +# This file is generated by running hack/bin/generate-local-nix-packages.sh and +# must be regenerated whenever local packages are added or removed, or +# dependencies are added or removed. +{ mkDerivation +, aeson +, aeson-pretty +, base +, bytestring +, cassandra-util +, conduit +, containers +, cql +, exceptions +, gitignoreSource +, imports +, lens +, lib +, optparse-applicative +, time +, tinylog +, types-common +, wire-api +}: +mkDerivation { + pname = "conference-feature"; + version = "1.0.0"; + src = gitignoreSource ./.; + isLibrary = true; + isExecutable = true; + libraryHaskellDepends = [ + aeson + aeson-pretty + base + bytestring + cassandra-util + conduit + containers + cql + exceptions + imports + lens + optparse-applicative + time + tinylog + types-common + wire-api + ]; + executableHaskellDepends = [ base ]; + description = "check the user's conference feature"; + license = lib.licenses.agpl3Only; + mainProgram = "conference-feature"; +} diff --git a/tools/db/conference-feature/src/ConferenceFeature/Lib.hs b/tools/db/conference-feature/src/ConferenceFeature/Lib.hs new file mode 100644 index 00000000000..c5098862e4d --- /dev/null +++ b/tools/db/conference-feature/src/ConferenceFeature/Lib.hs @@ -0,0 +1,95 @@ +{-# LANGUAGE OverloadedStrings #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2024 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module ConferenceFeature.Lib where + +import Cassandra as C +import ConferenceFeature.Types +import Data.Conduit +import qualified Data.Conduit.Combinators as Conduit +import qualified Data.Set as Set +import qualified Database.CQL.Protocol as CQL +import Imports +import Options.Applicative +import System.Logger.Class (MonadLogger) +import qualified System.Logger.Class as Log +import System.Logger.Message ((.=)) +import Wire.API.Team.Feature +import Wire.API.User (AccountStatus (Active)) + +pageSize :: Int32 +pageSize = 1000 + +readUsers :: forall m. (MonadClient m, MonadLogger m) => ConduitM () [UserRow] m () +readUsers = + paginateC cql (paramsP One () pageSize) x5 + .| Conduit.map (fmap CQL.asRecord) + where + cql :: C.PrepQuery C.R () (CQL.TupleType UserRow) + cql = + "SELECT id, activated, status, team, feature_conference_calling FROM user" + +scanUsers :: forall m. (MonadClient m, MonadLogger m) => ConduitT () Result m () +scanUsers = do + readUsers + .| Conduit.concat + .| Conduit.map userToResult + .| Conduit.scanl (<>) mempty + .| Conduit.iterM (logEvery 100000) + where + logEvery :: Int -> Result -> m () + logEvery i r = + when ((getSum (usersTotal r)) `mod` i == 0) $ Log.info $ "intermediate" .= show r + +userToResult :: UserRow -> Result +userToResult user = case user.conferenceCallingFeatureStatus of + Just FeatureStatusEnabled -> + if user.activated && user.status == Just Active + then case user.team of + Just tid -> + mempty + { usersTotal = Sum 1, + activeTeamUsersWithConferenceCalling = Sum 1, + affectedTeams = Set.singleton tid + } + Nothing -> + mempty + { usersTotal = Sum 1, + activeFreeUsersWithConferenceCalling = Sum 1 + } + else + mempty + { usersTotal = Sum 1, + inactiveUsersWithConferenceCalling = Sum 1 + } + Just FeatureStatusDisabled -> mempty {usersTotal = Sum 1, usersWithConferenceCallingDisabled = Sum 1} + Nothing -> mempty {usersTotal = Sum 1} + +run :: forall m. (MonadClient m, MonadLogger m) => m () +run = do + result <- runConduit $ scanUsers .| Conduit.lastDef mempty + Log.info $ "result" .= show result + +main :: IO () +main = do + opts <- execParser (info (helper <*> optsParser) desc) + env <- mkEnv opts + runReaderT (unAppT run) env + where + desc = header "conference-feature" <> progDesc "find users with conference feature" <> fullDesc diff --git a/tools/db/conference-feature/src/ConferenceFeature/Types.hs b/tools/db/conference-feature/src/ConferenceFeature/Types.hs new file mode 100644 index 00000000000..e667a1345a2 --- /dev/null +++ b/tools/db/conference-feature/src/ConferenceFeature/Types.hs @@ -0,0 +1,172 @@ +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE TemplateHaskell #-} +{-# OPTIONS_GHC -Wno-orphans #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2024 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module ConferenceFeature.Types where + +import Cassandra as C hiding (Set) +import qualified Cassandra.Settings as C +import Control.Lens +import Control.Monad.Catch (MonadThrow) +import qualified Data.Aeson as A +import qualified Data.Aeson.Encode.Pretty as A +import qualified Data.ByteString.Lazy.Char8 as LC8 +import Data.Id +import Data.Monoid +import Data.Text.Strict.Lens +import Database.CQL.Protocol hiding (Result, Set) +import Imports hiding (Sum) +import Options.Applicative +import qualified System.Logger as Log +import System.Logger.Class (MonadLogger, log) +import Wire.API.Team.Feature (FeatureStatus) +import Wire.API.User + +data Env = Env + { casClient :: C.ClientState, + logger :: Log.Logger + } + +newtype AppT m a = AppT {unAppT :: ReaderT Env m a} + deriving + ( Functor, + Applicative, + Monad, + MonadIO, + MonadThrow, + MonadReader Env, + MonadUnliftIO + ) + +instance MonadTrans AppT where + lift = AppT . lift + +instance (MonadIO m, MonadThrow m) => C.MonadClient (AppT m) where + liftClient m = do + env <- ask + lift . C.runClient env.casClient $ m + localState f = local (\env -> env {casClient = f $ env.casClient}) + +instance (MonadIO m) => MonadLogger (AppT m) where + log level f = do + env <- ask + lift $ Log.log (env.logger) level f + +mkEnv :: Opts -> IO Env +mkEnv opts = do + logger <- initLogger + brigClient <- initCas opts.brigDb logger + pure $ Env brigClient logger + where + initLogger = + Log.new + . Log.setOutput Log.StdOut + . Log.setFormat Nothing + . Log.setBufSize 0 + $ Log.defSettings + + initCas settings l = + C.init + . C.setLogger (C.mkLogger l) + . C.setContacts settings.host [] + . C.setPortNumber (fromIntegral settings.port) + . C.setKeyspace settings.keyspace + . C.setProtocolVersion C.V4 + $ C.defSettings + +data CassandraSettings = CassandraSettings + { host :: String, + port :: Int, + keyspace :: C.Keyspace + } + +data Opts = Opts + { brigDb :: CassandraSettings + } + +optsParser :: Parser Opts +optsParser = + Opts <$> brigCassandraParser + +brigCassandraParser :: Parser CassandraSettings +brigCassandraParser = + CassandraSettings + <$> strOption + ( long "host" + <> metavar "HOST" + <> help "Cassandra Host for brig" + <> value "localhost" + <> showDefault + ) + <*> option + auto + ( long "port" + <> metavar "PORT" + <> help "Cassandra Port for brig" + <> value 9042 + <> showDefault + ) + <*> ( C.Keyspace + . view packed + <$> strOption + ( long "keyspace" + <> metavar "STRING" + <> help "Cassandra Keyspace for brig" + <> value "brig_test" + <> showDefault + ) + ) + +type Activated = Bool + +data Result = Result + { usersTotal :: Sum Int, + activeFreeUsersWithConferenceCalling :: Sum Int, + activeTeamUsersWithConferenceCalling :: Sum Int, + inactiveUsersWithConferenceCalling :: Sum Int, + usersWithConferenceCallingDisabled :: Sum Int, + affectedTeams :: Set TeamId + } + deriving (Generic, Eq) + +instance Semigroup Result where + Result a1 a2 a3 a4 a5 a6 <> Result b1 b2 b3 b4 b5 b6 = Result (a1 <> b1) (a2 <> b2) (a3 <> b3) (a4 <> b4) (a5 <> b5) (a6 <> b6) + +instance Monoid Result where + mempty = Result mempty mempty mempty mempty mempty mempty + +data UserRow = UserRow + { id :: UserId, + activated :: Activated, + status :: Maybe AccountStatus, + team :: Maybe TeamId, + conferenceCallingFeatureStatus :: Maybe FeatureStatus + } + deriving (Generic) + +recordInstance ''UserRow + +instance A.ToJSON (Sum Int) where + toJSON (Sum i) = A.toJSON i + +instance A.ToJSON Result + +instance Show Result where + show = LC8.unpack . A.encodePretty