From e05e96bfe8a8f4b15ce5bf4bba2e6b01fc344401 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Thu, 30 Sep 2021 10:04:36 +0200 Subject: [PATCH 01/27] Extract function to create UserList --- services/galley/src/Galley/API/Create.hs | 17 +++++++---------- services/galley/src/Galley/Types/UserList.hs | 7 +++++++ 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/services/galley/src/Galley/API/Create.hs b/services/galley/src/Galley/API/Create.hs index b5581f1b6cf..7930fa25127 100644 --- a/services/galley/src/Galley/API/Create.hs +++ b/services/galley/src/Galley/API/Create.hs @@ -96,11 +96,7 @@ createRegularGroupConv :: UserId -> ConnId -> NewConvUnmanaged -> Galley Convers createRegularGroupConv zusr zcon (NewConvUnmanaged body) = do lusr <- qualifyLocal zusr name <- rangeCheckedMaybe (newConvName body) - let unqualifiedUserIds = newConvUsers body - qualifiedUserIds = newConvQualifiedUsers body - allUsers = - toUserList lusr $ - map (qUntagged . qualifyAs lusr) unqualifiedUserIds <> qualifiedUserIds + let allUsers = newConvMembers lusr body checkedUsers <- checkedConvSize allUsers ensureConnected zusr (ulLocals allUsers) checkRemoteUsersExist (ulRemotes allUsers) @@ -125,11 +121,7 @@ createTeamGroupConv :: UserId -> ConnId -> Public.ConvTeamInfo -> Public.NewConv createTeamGroupConv zusr zcon tinfo body = do lusr <- qualifyLocal zusr name <- rangeCheckedMaybe (newConvName body) - let unqualifiedUserIds = newConvUsers body - qualifiedUserIds = newConvQualifiedUsers body - allUsers = - toUserList lusr $ - map (qUntagged . qualifyAs lusr) unqualifiedUserIds <> qualifiedUserIds + let allUsers = newConvMembers lusr body convTeam = cnvTeamId tinfo zusrMembership <- Data.teamMember convTeam zusr @@ -343,3 +335,8 @@ access :: NewConv -> [Access] access a = case Set.toList (newConvAccess a) of [] -> Data.defRegularConvAccess (x : xs) -> x : xs + +newConvMembers :: Local x -> NewConv -> UserList UserId +newConvMembers loc body = + UserList (newConvUsers body) [] + <> toUserList loc (newConvQualifiedUsers body) diff --git a/services/galley/src/Galley/Types/UserList.hs b/services/galley/src/Galley/Types/UserList.hs index ffcabd6984e..c63148d6b97 100644 --- a/services/galley/src/Galley/Types/UserList.hs +++ b/services/galley/src/Galley/Types/UserList.hs @@ -33,6 +33,13 @@ data UserList a = UserList } deriving (Functor, Foldable, Traversable) +instance Semigroup (UserList a) where + UserList locals1 remotes1 <> UserList locals2 remotes2 = + UserList (locals1 <> locals2) (remotes1 <> remotes2) + +instance Monoid (UserList a) where + mempty = UserList mempty mempty + toUserList :: Foldable f => Local x -> f (Qualified a) -> UserList a toUserList loc = uncurry UserList . partitionQualified loc From 56e330f3ef4313b25ca978e0ffd6a4af3ff2512c Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Thu, 30 Sep 2021 10:38:40 +0200 Subject: [PATCH 02/27] Add stub for remote 1-1 conversation creation --- services/galley/src/Galley/API/Create.hs | 55 +++++++++++++++++------- 1 file changed, 39 insertions(+), 16 deletions(-) diff --git a/services/galley/src/Galley/API/Create.hs b/services/galley/src/Galley/API/Create.hs index 7930fa25127..ca0f2a821ca 100644 --- a/services/galley/src/Galley/API/Create.hs +++ b/services/galley/src/Galley/API/Create.hs @@ -51,6 +51,7 @@ import Network.Wai.Predicate hiding (setStatus) import Network.Wai.Utilities import qualified Wire.API.Conversation as Public import Wire.API.ErrorDescription (MissingLegalholdConsent) +import Wire.API.Federation.Error (federationNotImplemented) import Wire.API.Routes.Public.Galley (ConversationResponse) import Wire.API.Routes.Public.Util import Wire.API.Team.LegalHold (LegalholdProtectee (LegalholdPlusFederationNotImplemented)) @@ -178,39 +179,57 @@ createSelfConversation zusr = do createOne2OneConversation :: UserId -> ConnId -> NewConvUnmanaged -> Galley ConversationResponse createOne2OneConversation zusr zcon (NewConvUnmanaged j) = do lusr <- qualifyLocal zusr - otherUserId <- head . fromRange <$> (rangeChecked (newConvUsers j) :: Galley (Range 1 1 [UserId])) - (x, y) <- toUUIDs zusr otherUserId - when (x == y) $ - throwM $ - invalidOp "Cannot create a 1-1 with yourself" - case newConvTeam j of + let allUsers = newConvMembers lusr j + other <- ensureOne (ulAll lusr allUsers) + when (unTagged lusr == other) $ + throwM (invalidOp "Cannot create a 1-1 with yourself") + foldQualified + lusr + (createLocalOne2OneConversation lusr zcon (newConvName j) (newConvTeam j)) + (createRemoteOne2OneConversation lusr zcon) + other + +createLocalOne2OneConversation :: + Local UserId -> + ConnId -> + Maybe Text -> + Maybe ConvTeamInfo -> + Local UserId -> + Galley ConversationResponse +createLocalOne2OneConversation lusr zcon name mteam lother = do + (x, y) <- toUUIDs (lUnqualified lusr) (lUnqualified lother) + case mteam of Just ti | cnvManaged ti -> throwM noManagedTeamConv | otherwise -> - checkBindingTeamPermissions zusr otherUserId (cnvTeamId ti) + checkBindingTeamPermissions lusr lother (cnvTeamId ti) Nothing -> do - ensureConnected zusr [otherUserId] - n <- rangeCheckedMaybe (newConvName j) + ensureConnected (lUnqualified lusr) [lUnqualified lother] + n <- rangeCheckedMaybe name c <- Data.conversation (Data.one2OneConvId x y) - maybe (create lusr x y n $ newConvTeam j) (conversationExisted zusr) c + maybe (create x y n mteam) (conversationExisted (lUnqualified lusr)) c where verifyMembership tid u = do membership <- Data.teamMember tid u when (isNothing membership) $ throwM noBindingTeamMembers checkBindingTeamPermissions x y tid = do - zusrMembership <- Data.teamMember tid zusr + zusrMembership <- Data.teamMember tid (lUnqualified lusr) void $ permissionCheck CreateConversation zusrMembership Data.teamBinding tid >>= \case Just Binding -> do - verifyMembership tid x - verifyMembership tid y + verifyMembership tid (lUnqualified x) + verifyMembership tid (lUnqualified y) Just _ -> throwM nonBindingTeam Nothing -> throwM teamNotFound - create lusr x y n tinfo = do + create x y n tinfo = do c <- Data.createOne2OneConversation lusr x y n (cnvTeamId <$> tinfo) - notifyCreatedConversation Nothing zusr (Just zcon) c - conversationCreated zusr c + notifyCreatedConversation Nothing (lUnqualified lusr) (Just zcon) c + conversationCreated (lUnqualified lusr) c + +createRemoteOne2OneConversation :: + Local UserId -> ConnId -> Remote UserId -> Galley ConversationResponse +createRemoteOne2OneConversation _ _ _ = throwM federationNotImplemented createConnectConversationH :: UserId ::: Maybe ConnId ::: JsonRequest Connect -> Galley Response createConnectConversationH (usr ::: conn ::: req) = do @@ -340,3 +359,7 @@ newConvMembers :: Local x -> NewConv -> UserList UserId newConvMembers loc body = UserList (newConvUsers body) [] <> toUserList loc (newConvQualifiedUsers body) + +ensureOne :: [a] -> Galley a +ensureOne [x] = pure x +ensureOne _ = throwM (invalidRange "One-to-one conversations can only have a single invited member") From 8310966f5ed40d14bf5ece2cfbf464e57daa9868 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Thu, 30 Sep 2021 12:54:11 +0200 Subject: [PATCH 03/27] Compute remote 1-1 conversation IDs --- services/galley/galley.cabal | 6 +- services/galley/package.yaml | 3 + services/galley/src/Galley/API/Create.hs | 6 +- services/galley/src/Galley/API/One2One.hs | 132 ++++++++++++++++++++++ 4 files changed, 141 insertions(+), 6 deletions(-) create mode 100644 services/galley/src/Galley/API/One2One.hs diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal index a43ea917332..da44789be72 100644 --- a/services/galley/galley.cabal +++ b/services/galley/galley.cabal @@ -4,7 +4,7 @@ cabal-version: 1.12 -- -- see: https://github.com/sol/hpack -- --- hash: a5b2ec0bd44d4fcabec564b4e7683a01cfb75cdb1c78a6eee520d6c48c95bb1d +-- hash: 7ca3c61253dfbb1c69f3cf336a7bf166a1b50d9f92bc327012b395045112d7b7 name: galley version: 0.83.0 @@ -35,6 +35,7 @@ library Galley.API.LegalHold.Conflicts Galley.API.Mapping Galley.API.Message + Galley.API.One2One Galley.API.Public Galley.API.Query Galley.API.Teams @@ -87,6 +88,7 @@ library , base >=4.6 && <5 , base64-bytestring >=1.0 , bilge >=0.21.1 + , binary , brig-types >=0.73.1 , bytestring >=0.9 , bytestring-conversion >=0.2 @@ -95,6 +97,7 @@ library , cassava >=0.5.2 , cereal >=0.4 , containers >=0.5 + , cryptonite , currency-codes >=2.0 , data-default >=0.5 , enclosed-exceptions >=1.0 @@ -113,6 +116,7 @@ library , imports , insert-ordered-containers , lens >=4.4 + , memory , metrics-wai >=0.4 , mtl >=2.2 , optparse-applicative >=0.10 diff --git a/services/galley/package.yaml b/services/galley/package.yaml index a4bda422abc..a1338730634 100644 --- a/services/galley/package.yaml +++ b/services/galley/package.yaml @@ -31,6 +31,7 @@ library: - base >=4.6 && <5 - base64-bytestring >=1.0 - bilge >=0.21.1 + - binary - brig-types >=0.73.1 - bytestring >=0.9 - bytestring-conversion >=0.2 @@ -38,6 +39,7 @@ library: - cassava >= 0.5.2 - cereal >=0.4 - containers >=0.5 + - cryptonite - currency-codes >=2.0 - data-default >=0.5 - enclosed-exceptions >=1.0 @@ -56,6 +58,7 @@ library: - http2-client-grpc - insert-ordered-containers - lens >=4.4 + - memory - metrics-wai >=0.4 - mtl >=2.2 - optparse-applicative >=0.10 diff --git a/services/galley/src/Galley/API/Create.hs b/services/galley/src/Galley/API/Create.hs index ca0f2a821ca..686e2a418ff 100644 --- a/services/galley/src/Galley/API/Create.hs +++ b/services/galley/src/Galley/API/Create.hs @@ -36,6 +36,7 @@ import Data.Time import qualified Data.UUID.Tagged as U import Galley.API.Error import Galley.API.Mapping +import Galley.API.One2One import Galley.API.Util import Galley.App import qualified Galley.Data as Data @@ -51,7 +52,6 @@ import Network.Wai.Predicate hiding (setStatus) import Network.Wai.Utilities import qualified Wire.API.Conversation as Public import Wire.API.ErrorDescription (MissingLegalholdConsent) -import Wire.API.Federation.Error (federationNotImplemented) import Wire.API.Routes.Public.Galley (ConversationResponse) import Wire.API.Routes.Public.Util import Wire.API.Team.LegalHold (LegalholdProtectee (LegalholdPlusFederationNotImplemented)) @@ -227,10 +227,6 @@ createLocalOne2OneConversation lusr zcon name mteam lother = do notifyCreatedConversation Nothing (lUnqualified lusr) (Just zcon) c conversationCreated (lUnqualified lusr) c -createRemoteOne2OneConversation :: - Local UserId -> ConnId -> Remote UserId -> Galley ConversationResponse -createRemoteOne2OneConversation _ _ _ = throwM federationNotImplemented - createConnectConversationH :: UserId ::: Maybe ConnId ::: JsonRequest Connect -> Galley Response createConnectConversationH (usr ::: conn ::: req) = do j <- fromJsonBody req diff --git a/services/galley/src/Galley/API/One2One.hs b/services/galley/src/Galley/API/One2One.hs new file mode 100644 index 00000000000..3cbfa901bb7 --- /dev/null +++ b/services/galley/src/Galley/API/One2One.hs @@ -0,0 +1,132 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2021 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 Galley.API.One2One + ( convId, + createRemoteOne2OneConversation, + ) +where + +import Control.Error (atMay) +import Control.Monad.Catch +import qualified Crypto.Hash as Crypto +import Data.Bits +import Data.ByteArray (convert) +import qualified Data.ByteString as B +import Data.ByteString.Conversion +import qualified Data.ByteString.Lazy as L +import Data.Id +import Data.Qualified +import Data.UUID (UUID) +import qualified Data.UUID as UUID +import Galley.App +import Imports +import Wire.API.Federation.Error (federationNotImplemented) +import Wire.API.Routes.Public.Galley (ConversationResponse) + +-- | The hash function used to obtain the 1-1 conversation ID for a pair of users. +-- +-- /Note/: the hash function must always return byte strings of length > 16. +hash :: ByteString -> ByteString +hash = convert . Crypto.hash @ByteString @Crypto.SHA256 + +-- | A randomly-generated UUID to use as a namespace for the UUIDv5 of 1-1 +-- conversation IDs +namespace :: UUID +namespace = UUID.fromWords 0x9a51edb8 0x060c0d9a 0x0c2950a8 0x5d152982 + +compareDomains :: Ord a => Qualified a -> Qualified a -> Ordering +compareDomains (Qualified a1 dom1) (Qualified a2 dom2) = + compare (dom1, a1) (dom2, a2) + +quidToByteString :: Qualified UserId -> ByteString +quidToByteString (Qualified uid domain) = toByteString' uid <> toByteString' domain + +setUUIDv5 :: UUID -> UUID +setUUIDv5 x = case UUID.toWords x of + (w0, w1, w2, w3) -> + UUID.fromWords + w0 + (w1 .&. 0xffff0fff .|. 0x5000) + (w2 .&. 0x3fffffff .|. 0x80000000) + w3 + +-- | This function returns the 1-1 conversation for a given pair of users. +-- +-- Let A, B denote the (not necessarily distinct) backends of the two users, +-- with the domain of A less or equal than the domain of B in the lexicographic +-- ordering of their ascii encodings. Given users a@A and b@B, the UUID and +-- owning domain of the unique 1-1 conversation between a and b shall be a +-- deterministic function of the input data, plus some fixed parameters, as +-- described below. +-- +-- __Parameters__ +-- +-- * A (collision-resistant) hash function h with N bits of output, where N +-- s a multiple of 8 strictly larger than 128; this is set to SHA256. +-- * A "namespace" UUID n. +-- +-- __Algorithm__ +-- +-- First, in the special case where A and B are the same backend, assume that +-- the UUID of a is lower than that of b. If that is not the case, swap a +-- and b in the following. This is necessary to ensure that the function we +-- describe below is symmetric in its arguments. +-- Let c be the bytestring obtained as the concatenation of the following 5 +-- components: +-- +-- * the 16 bytes of the namespace n +-- * the 16 bytes of the UUID of a +-- * the ascii encoding of the domain of A +-- * the 16 bytes of the UUID of b +-- * the ascii encoding of the domain of B, +-- +-- and let x = h(c) be its hashed value. The UUID of the 1-1 conversation +-- between a and b is obtained by converting the first 128 bits of x to a UUID +-- V5. Note that our use of V5 here is not strictly compliant with RFC 4122, +-- since we are using a custom hash and not necessarily SHA1. +-- +-- The owning domain for the conversation is set to be A if bit 128 of x (i.e. +-- the most significant bit of the octet at index 16) is 0, and B otherwise. +-- This is well-defined, because we assumed the number of bits of x to be +-- strictly larger than 128. +convId :: Qualified UserId -> Qualified UserId -> Qualified ConvId +convId a b = case compareDomains a b of + GT -> convId b a + _ -> + let c = + mconcat + [ L.toStrict (UUID.toByteString namespace), + quidToByteString a, + quidToByteString b + ] + x = hash c + result = + setUUIDv5 + . fromMaybe UUID.nil + . UUID.fromByteString + . L.fromStrict + . B.take 16 + $ x + domain + | (fromMaybe 0 (atMay (B.unpack x) 16)) .&. 0x80 == 0 = qDomain a + | otherwise = qDomain b + in Qualified (Id result) domain + +createRemoteOne2OneConversation :: + Local UserId -> ConnId -> Remote UserId -> Galley ConversationResponse +createRemoteOne2OneConversation _ _ _ = throwM federationNotImplemented From 2be6a6a7af4e938d1f5ea57555a949d1650c2901 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Thu, 30 Sep 2021 13:38:06 +0200 Subject: [PATCH 04/27] ensureConnected now takes a UserList --- services/galley/src/Galley/API/Create.hs | 4 ++-- services/galley/src/Galley/API/One2One.hs | 6 +++++- services/galley/src/Galley/API/Util.hs | 9 ++++----- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/services/galley/src/Galley/API/Create.hs b/services/galley/src/Galley/API/Create.hs index 686e2a418ff..ac7c9d54342 100644 --- a/services/galley/src/Galley/API/Create.hs +++ b/services/galley/src/Galley/API/Create.hs @@ -99,7 +99,7 @@ createRegularGroupConv zusr zcon (NewConvUnmanaged body) = do name <- rangeCheckedMaybe (newConvName body) let allUsers = newConvMembers lusr body checkedUsers <- checkedConvSize allUsers - ensureConnected zusr (ulLocals allUsers) + ensureConnected lusr allUsers checkRemoteUsersExist (ulRemotes allUsers) ensureNoLegalholdConflicts (ulRemotes allUsers) (ulLocals allUsers) c <- @@ -204,7 +204,7 @@ createLocalOne2OneConversation lusr zcon name mteam lother = do | otherwise -> checkBindingTeamPermissions lusr lother (cnvTeamId ti) Nothing -> do - ensureConnected (lUnqualified lusr) [lUnqualified lother] + ensureConnectedToLocals (lUnqualified lusr) [lUnqualified lother] n <- rangeCheckedMaybe name c <- Data.conversation (Data.one2OneConvId x y) maybe (create x y n mteam) (conversationExisted (lUnqualified lusr)) c diff --git a/services/galley/src/Galley/API/One2One.hs b/services/galley/src/Galley/API/One2One.hs index 3cbfa901bb7..52b52d27d3e 100644 --- a/services/galley/src/Galley/API/One2One.hs +++ b/services/galley/src/Galley/API/One2One.hs @@ -33,7 +33,9 @@ import Data.Id import Data.Qualified import Data.UUID (UUID) import qualified Data.UUID as UUID +import Galley.API.Util import Galley.App +import Galley.Types.UserList import Imports import Wire.API.Federation.Error (federationNotImplemented) import Wire.API.Routes.Public.Galley (ConversationResponse) @@ -129,4 +131,6 @@ convId a b = case compareDomains a b of createRemoteOne2OneConversation :: Local UserId -> ConnId -> Remote UserId -> Galley ConversationResponse -createRemoteOne2OneConversation _ _ _ = throwM federationNotImplemented +createRemoteOne2OneConversation self _con rother = do + ensureConnected self (UserList [] [rother]) + throwM federationNotImplemented diff --git a/services/galley/src/Galley/API/Util.hs b/services/galley/src/Galley/API/Util.hs index 045588dc954..0586568e9e0 100644 --- a/services/galley/src/Galley/API/Util.hs +++ b/services/galley/src/Galley/API/Util.hs @@ -99,18 +99,17 @@ ensureConnectedOrSameTeam (Qualified u domain) uids = do sameTeamUids <- forM uTeams $ \team -> fmap (view userId) <$> Data.teamMembersLimited team uids -- Do not check connections for users that are on the same team - ensureConnected u (uids \\ join sameTeamUids) + ensureConnectedToLocals u (uids \\ join sameTeamUids) -- | Check that the user is connected to everybody else. -- -- The connection has to be bidirectional (e.g. if A connects to B and later -- B blocks A, the status of A-to-B is still 'Accepted' but it doesn't mean -- that they are connected). -ensureConnected :: UserId -> [UserId] -> Galley () -ensureConnected _ [] = pure () -ensureConnected u localUserIds = do +ensureConnected :: Local UserId -> UserList UserId -> Galley () +ensureConnected self others = do -- FUTUREWORK(federation, #1262): check remote connections - ensureConnectedToLocals u localUserIds + ensureConnectedToLocals (lUnqualified self) (ulLocals others) ensureConnectedToLocals :: UserId -> [UserId] -> Galley () ensureConnectedToLocals _ [] = pure () From ae0bbd00aa52d05851a58e0981c72908757de92d Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Thu, 30 Sep 2021 16:00:11 +0200 Subject: [PATCH 05/27] Make /conversations/one2one federation-aware Converted the endpoint for creating 1-1 conversations to the new conversation ID algorithm, and enabled the endpoint to create 1-1 conversations with federated users. Note: the case when the conversation needs to be hosted by the remote domain is still not implemented. We probably need a new RPC for this case. --- services/galley/src/Galley/API/Create.hs | 113 ++++++++++++++++------ services/galley/src/Galley/API/One2One.hs | 24 +---- services/galley/src/Galley/Data.hs | 45 ++++++--- 3 files changed, 122 insertions(+), 60 deletions(-) diff --git a/services/galley/src/Galley/API/Create.hs b/services/galley/src/Galley/API/Create.hs index ac7c9d54342..4f6394c35d0 100644 --- a/services/galley/src/Galley/API/Create.hs +++ b/services/galley/src/Galley/API/Create.hs @@ -52,6 +52,7 @@ import Network.Wai.Predicate hiding (setStatus) import Network.Wai.Utilities import qualified Wire.API.Conversation as Public import Wire.API.ErrorDescription (MissingLegalholdConsent) +import Wire.API.Federation.Error (federationNotImplemented) import Wire.API.Routes.Public.Galley (ConversationResponse) import Wire.API.Routes.Public.Util import Wire.API.Team.LegalHold (LegalholdProtectee (LegalholdPlusFederationNotImplemented)) @@ -183,49 +184,98 @@ createOne2OneConversation zusr zcon (NewConvUnmanaged j) = do other <- ensureOne (ulAll lusr allUsers) when (unTagged lusr == other) $ throwM (invalidOp "Cannot create a 1-1 with yourself") + mtid <- case newConvTeam j of + Just ti + | cnvManaged ti -> throwM noManagedTeamConv + | otherwise -> do + foldQualified + lusr + (\lother -> checkBindingTeamPermissions lusr lother (cnvTeamId ti)) + (const (pure Nothing)) + other + Nothing -> ensureConnected lusr allUsers $> Nothing + n <- rangeCheckedMaybe (newConvName j) foldQualified lusr - (createLocalOne2OneConversation lusr zcon (newConvName j) (newConvTeam j)) - (createRemoteOne2OneConversation lusr zcon) + (createLegacyOne2OneConversationUnchecked lusr zcon n mtid) + (createOne2OneConversationUnchecked lusr zcon n mtid . unTagged) other - -createLocalOne2OneConversation :: - Local UserId -> - ConnId -> - Maybe Text -> - Maybe ConvTeamInfo -> - Local UserId -> - Galley ConversationResponse -createLocalOne2OneConversation lusr zcon name mteam lother = do - (x, y) <- toUUIDs (lUnqualified lusr) (lUnqualified lother) - case mteam of - Just ti - | cnvManaged ti -> throwM noManagedTeamConv - | otherwise -> - checkBindingTeamPermissions lusr lother (cnvTeamId ti) - Nothing -> do - ensureConnectedToLocals (lUnqualified lusr) [lUnqualified lother] - n <- rangeCheckedMaybe name - c <- Data.conversation (Data.one2OneConvId x y) - maybe (create x y n mteam) (conversationExisted (lUnqualified lusr)) c where verifyMembership tid u = do membership <- Data.teamMember tid u when (isNothing membership) $ throwM noBindingTeamMembers - checkBindingTeamPermissions x y tid = do + checkBindingTeamPermissions lusr lother tid = do zusrMembership <- Data.teamMember tid (lUnqualified lusr) void $ permissionCheck CreateConversation zusrMembership Data.teamBinding tid >>= \case Just Binding -> do - verifyMembership tid (lUnqualified x) - verifyMembership tid (lUnqualified y) + verifyMembership tid (lUnqualified lusr) + verifyMembership tid (lUnqualified lother) + pure (Just tid) Just _ -> throwM nonBindingTeam Nothing -> throwM teamNotFound - create x y n tinfo = do - c <- Data.createOne2OneConversation lusr x y n (cnvTeamId <$> tinfo) - notifyCreatedConversation Nothing (lUnqualified lusr) (Just zcon) c - conversationCreated (lUnqualified lusr) c + +createLegacyOne2OneConversationUnchecked :: + Local UserId -> + ConnId -> + Maybe (Range 1 256 Text) -> + Maybe TeamId -> + Local UserId -> + Galley ConversationResponse +createLegacyOne2OneConversationUnchecked self zcon name mtid other = do + lcnv <- localOne2OneConvId self other + mc <- Data.conversation (lUnqualified lcnv) + case mc of + Just c -> conversationExisted (lUnqualified self) c + Nothing -> do + (x, y) <- toUUIDs (lUnqualified self) (lUnqualified other) + c <- Data.createLegacyOne2OneConversation self x y name mtid + notifyCreatedConversation Nothing (lUnqualified self) (Just zcon) c + conversationCreated (lUnqualified self) c + +createOne2OneConversationUnchecked :: + Local UserId -> + ConnId -> + Maybe (Range 1 256 Text) -> + Maybe TeamId -> + Qualified UserId -> + Galley ConversationResponse +createOne2OneConversationUnchecked self zcon name mtid other = do + let create = + foldQualified + self + createOne2OneConversationLocally + createOne2OneConversationRemotely + create (one2OneConvId (unTagged self) other) self zcon name mtid other + +createOne2OneConversationLocally :: + Local ConvId -> + Local UserId -> + ConnId -> + Maybe (Range 1 256 Text) -> + Maybe TeamId -> + Qualified UserId -> + Galley ConversationResponse +createOne2OneConversationLocally lcnv self zcon name mtid other = do + mc <- Data.conversation (lUnqualified lcnv) + case mc of + Just c -> conversationExisted (lUnqualified self) c + Nothing -> do + c <- Data.createOne2OneConversation lcnv self other name mtid + notifyCreatedConversation Nothing (lUnqualified self) (Just zcon) c + conversationCreated (lUnqualified self) c + +createOne2OneConversationRemotely :: + Remote ConvId -> + Local UserId -> + ConnId -> + Maybe (Range 1 256 Text) -> + Maybe TeamId -> + Qualified UserId -> + Galley ConversationResponse +createOne2OneConversationRemotely _ _ _ _ _ _ = + throwM federationNotImplemented createConnectConversationH :: UserId ::: Maybe ConnId ::: JsonRequest Connect -> Galley Response createConnectConversationH (usr ::: conn ::: req) = do @@ -337,6 +387,11 @@ notifyCreatedConversation dtime usr conn c = do & pushConn .~ conn & pushRoute .~ route +localOne2OneConvId :: Local UserId -> Local UserId -> Galley (Local ConvId) +localOne2OneConvId self other = do + (x, y) <- toUUIDs (lUnqualified self) (lUnqualified other) + pure . qualifyAs self $ Data.one2OneConvId x y + toUUIDs :: UserId -> UserId -> Galley (U.UUID U.V4, U.UUID U.V4) toUUIDs a b = do a' <- U.fromUUID (toUUID a) & ifNothing invalidUUID4 diff --git a/services/galley/src/Galley/API/One2One.hs b/services/galley/src/Galley/API/One2One.hs index 52b52d27d3e..de2792e772c 100644 --- a/services/galley/src/Galley/API/One2One.hs +++ b/services/galley/src/Galley/API/One2One.hs @@ -15,14 +15,9 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Galley.API.One2One - ( convId, - createRemoteOne2OneConversation, - ) -where +module Galley.API.One2One (one2OneConvId) where import Control.Error (atMay) -import Control.Monad.Catch import qualified Crypto.Hash as Crypto import Data.Bits import Data.ByteArray (convert) @@ -33,12 +28,7 @@ import Data.Id import Data.Qualified import Data.UUID (UUID) import qualified Data.UUID as UUID -import Galley.API.Util -import Galley.App -import Galley.Types.UserList import Imports -import Wire.API.Federation.Error (federationNotImplemented) -import Wire.API.Routes.Public.Galley (ConversationResponse) -- | The hash function used to obtain the 1-1 conversation ID for a pair of users. -- @@ -106,9 +96,9 @@ setUUIDv5 x = case UUID.toWords x of -- the most significant bit of the octet at index 16) is 0, and B otherwise. -- This is well-defined, because we assumed the number of bits of x to be -- strictly larger than 128. -convId :: Qualified UserId -> Qualified UserId -> Qualified ConvId -convId a b = case compareDomains a b of - GT -> convId b a +one2OneConvId :: Qualified UserId -> Qualified UserId -> Qualified ConvId +one2OneConvId a b = case compareDomains a b of + GT -> one2OneConvId b a _ -> let c = mconcat @@ -128,9 +118,3 @@ convId a b = case compareDomains a b of | (fromMaybe 0 (atMay (B.unpack x) 16)) .&. 0x80 == 0 = qDomain a | otherwise = qDomain b in Qualified (Id result) domain - -createRemoteOne2OneConversation :: - Local UserId -> ConnId -> Remote UserId -> Galley ConversationResponse -createRemoteOne2OneConversation self _con rother = do - ensureConnected self (UserList [] [rother]) - throwM federationNotImplemented diff --git a/services/galley/src/Galley/Data.hs b/services/galley/src/Galley/Data.hs index 55be7a13813..3e24895b8ce 100644 --- a/services/galley/src/Galley/Data.hs +++ b/services/galley/src/Galley/Data.hs @@ -69,6 +69,7 @@ module Galley.Data conversationsRemote, createConnectConversation, createConversation, + createLegacyOne2OneConversation, createOne2OneConversation, createSelfConversation, isConvAlive, @@ -156,7 +157,14 @@ import Galley.Types.Clients (Clients) import qualified Galley.Types.Clients as Clients import Galley.Types.Conversations.Members import Galley.Types.Conversations.Roles -import Galley.Types.Teams hiding (Event, EventType (..), teamConversations, teamMembers) +import Galley.Types.Teams hiding + ( Event, + EventType (..), + self, + teamConversations, + teamMembers, + ) +import qualified Galley.Types.Teams as Teams import Galley.Types.Teams.Intra import Galley.Types.UserList import Galley.Validation @@ -435,7 +443,7 @@ updateTeamMember oldPerms tid uid newPerms = do when (SetBilling `Set.member` lostPerms) $ addPrepQuery Cql.deleteBillingTeamMember (tid, uid) where - permDiff = Set.difference `on` view self + permDiff = Set.difference `on` view Teams.self acquiredPerms = newPerms `permDiff` oldPerms lostPerms = oldPerms `permDiff` newPerms @@ -681,7 +689,7 @@ createConnectConversation loc a b name = do (lmems, rmems) <- addMembers lconv (UserList [a'] []) pure $ newConv conv ConnectConv a' lmems rmems [PrivateAccess] privateRole name Nothing Nothing Nothing -createOne2OneConversation :: +createLegacyOne2OneConversation :: MonadClient m => Local x -> U.UUID U.V4 -> @@ -689,21 +697,36 @@ createOne2OneConversation :: Maybe (Range 1 256 Text) -> Maybe TeamId -> m Conversation -createOne2OneConversation loc a b name ti = do +createLegacyOne2OneConversation loc a b name ti = do let conv = one2OneConvId a b lconv = qualifyAs loc conv a' = Id (U.unpack a) b' = Id (U.unpack b) - retry x5 $ case ti of - Nothing -> write Cql.insertConv (params Quorum (conv, One2OneConv, a', privateOnly, privateRole, fromRange <$> name, Nothing, Nothing, Nothing)) + createOne2OneConversation + lconv + (qualifyAs loc a') + (unTagged (qualifyAs loc b')) + name + ti + +createOne2OneConversation :: + MonadClient m => + Local ConvId -> + Local UserId -> + Qualified UserId -> + Maybe (Range 1 256 Text) -> + Maybe TeamId -> + m Conversation +createOne2OneConversation lconv self other name mtid = do + retry x5 $ case mtid of + Nothing -> write Cql.insertConv (params Quorum (lUnqualified lconv, One2OneConv, lUnqualified self, privateOnly, privateRole, fromRange <$> name, Nothing, Nothing, Nothing)) Just tid -> batch $ do setType BatchLogged setConsistency Quorum - addPrepQuery Cql.insertConv (conv, One2OneConv, a', privateOnly, privateRole, fromRange <$> name, Just tid, Nothing, Nothing) - addPrepQuery Cql.insertTeamConv (tid, conv, False) - -- FUTUREWORK: federated one2one - (lmems, rmems) <- addMembers lconv (UserList [a', b'] []) - pure $ newConv conv One2OneConv a' lmems rmems [PrivateAccess] privateRole name ti Nothing Nothing + addPrepQuery Cql.insertConv (lUnqualified lconv, One2OneConv, lUnqualified self, privateOnly, privateRole, fromRange <$> name, Just tid, Nothing, Nothing) + addPrepQuery Cql.insertTeamConv (tid, lUnqualified lconv, False) + (lmems, rmems) <- addMembers lconv (toUserList self [unTagged self, other]) + pure $ newConv (lUnqualified lconv) One2OneConv (lUnqualified self) lmems rmems [PrivateAccess] privateRole name mtid Nothing Nothing updateConversation :: MonadClient m => ConvId -> Range 1 256 Text -> m () updateConversation cid name = retry x5 $ write Cql.updateConvName (params Quorum (fromRange name, cid)) From 6536bca9ed0f923d3c7ec84a95f0398e393e3243 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Fri, 1 Oct 2021 08:54:30 +0200 Subject: [PATCH 06/27] Remove create from UUID Version class The create function cannot be defined for all UUID versions. --- libs/types-common/src/Data/UUID/Tagged.hs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/libs/types-common/src/Data/UUID/Tagged.hs b/libs/types-common/src/Data/UUID/Tagged.hs index cd822e7d04a..b9420213040 100644 --- a/libs/types-common/src/Data/UUID/Tagged.hs +++ b/libs/types-common/src/Data/UUID/Tagged.hs @@ -23,6 +23,7 @@ module Data.UUID.Tagged variant, addv4, unpack, + create, ) where @@ -38,20 +39,20 @@ newtype UUID v = UUID D.UUID deriving (Eq, Ord, Show) instance NFData (UUID v) where rnf (UUID a) = seq a () class Version v where - -- | Create a fresh versioned UUID. - create :: IO (UUID v) - -- | Try to turn a plain UUID into a versioned UUID. fromUUID :: D.UUID -> Maybe (UUID v) data V4 instance Version V4 where - create = UUID <$> D4.nextRandom fromUUID u = case version u of 4 -> Just (UUID u) _ -> Nothing +-- | Create a fresh UUIDv4. +create :: IO (UUID V4) +create = UUID <$> D4.nextRandom + instance Arbitrary (UUID V4) where arbitrary = do a <- arbitrary From e5a417a3377f36c39808b29168c6ce8768e299f9 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Fri, 1 Oct 2021 09:14:12 +0200 Subject: [PATCH 07/27] Introduce V5 UUIDs and use them for 1-1 conv --- libs/types-common/src/Data/UUID/Tagged.hs | 37 +++++++++++++++-------- services/galley/src/Galley/API/One2One.hs | 12 ++------ 2 files changed, 26 insertions(+), 23 deletions(-) diff --git a/libs/types-common/src/Data/UUID/Tagged.hs b/libs/types-common/src/Data/UUID/Tagged.hs index b9420213040..e3552f0b0ba 100644 --- a/libs/types-common/src/Data/UUID/Tagged.hs +++ b/libs/types-common/src/Data/UUID/Tagged.hs @@ -17,13 +17,16 @@ module Data.UUID.Tagged ( UUID, + toUUID, V4, + V5, Version (..), version, variant, addv4, unpack, create, + mk, ) where @@ -31,36 +34,44 @@ import Data.Bits import qualified Data.UUID as D import qualified Data.UUID.V4 as D4 import Imports -import Test.QuickCheck (Arbitrary, arbitrary) -- | Versioned UUID. -newtype UUID v = UUID D.UUID deriving (Eq, Ord, Show) +newtype UUID v = UUID {toUUID :: D.UUID} + deriving (Eq, Ord, Show) instance NFData (UUID v) where rnf (UUID a) = seq a () class Version v where -- | Try to turn a plain UUID into a versioned UUID. fromUUID :: D.UUID -> Maybe (UUID v) + fromUUID u = guard (version u == versionValue @v) $> UUID u + + versionValue :: Word32 data V4 instance Version V4 where - fromUUID u = case version u of - 4 -> Just (UUID u) - _ -> Nothing + versionValue = 4 + +data V5 + +instance Version V5 where + versionValue = 5 + +mk :: forall v. Version v => D.UUID -> UUID v +mk u = UUID $ + case D.toWords u of + (x0, x1, x2, x3) -> + D.fromWords + x0 + (retainVersion (versionValue @v) x1) + (retainVariant 2 x2) + x3 -- | Create a fresh UUIDv4. create :: IO (UUID V4) create = UUID <$> D4.nextRandom -instance Arbitrary (UUID V4) where - arbitrary = do - a <- arbitrary - b <- retainVersion 4 <$> arbitrary - c <- retainVariant 2 <$> arbitrary - d <- arbitrary - pure $ UUID $ D.fromWords a b c d - -- | Extract the 'D.UUID' from a versioned UUID. unpack :: UUID v -> D.UUID unpack (UUID x) = x diff --git a/services/galley/src/Galley/API/One2One.hs b/services/galley/src/Galley/API/One2One.hs index de2792e772c..daa542a6983 100644 --- a/services/galley/src/Galley/API/One2One.hs +++ b/services/galley/src/Galley/API/One2One.hs @@ -28,6 +28,7 @@ import Data.Id import Data.Qualified import Data.UUID (UUID) import qualified Data.UUID as UUID +import qualified Data.UUID.Tagged as U import Imports -- | The hash function used to obtain the 1-1 conversation ID for a pair of users. @@ -48,15 +49,6 @@ compareDomains (Qualified a1 dom1) (Qualified a2 dom2) = quidToByteString :: Qualified UserId -> ByteString quidToByteString (Qualified uid domain) = toByteString' uid <> toByteString' domain -setUUIDv5 :: UUID -> UUID -setUUIDv5 x = case UUID.toWords x of - (w0, w1, w2, w3) -> - UUID.fromWords - w0 - (w1 .&. 0xffff0fff .|. 0x5000) - (w2 .&. 0x3fffffff .|. 0x80000000) - w3 - -- | This function returns the 1-1 conversation for a given pair of users. -- -- Let A, B denote the (not necessarily distinct) backends of the two users, @@ -108,7 +100,7 @@ one2OneConvId a b = case compareDomains a b of ] x = hash c result = - setUUIDv5 + U.toUUID . U.mk @U.V5 . fromMaybe UUID.nil . UUID.fromByteString . L.fromStrict From 92231a5913d286a95a3b2b920b07bcfe8a0b9b01 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Fri, 1 Oct 2021 09:55:19 +0200 Subject: [PATCH 08/27] Servantify internal endpoint for connect conv --- services/galley/src/Galley/API/Create.hs | 13 +++++------ services/galley/src/Galley/API/Internal.hs | 27 ++++++++++++++-------- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/services/galley/src/Galley/API/Create.hs b/services/galley/src/Galley/API/Create.hs index 4f6394c35d0..f0aa268bde4 100644 --- a/services/galley/src/Galley/API/Create.hs +++ b/services/galley/src/Galley/API/Create.hs @@ -20,7 +20,7 @@ module Galley.API.Create internalCreateManagedConversationH, createSelfConversation, createOne2OneConversation, - createConnectConversationH, + createConnectConversation, ) where @@ -277,12 +277,11 @@ createOne2OneConversationRemotely :: createOne2OneConversationRemotely _ _ _ _ _ _ = throwM federationNotImplemented -createConnectConversationH :: UserId ::: Maybe ConnId ::: JsonRequest Connect -> Galley Response -createConnectConversationH (usr ::: conn ::: req) = do - j <- fromJsonBody req - handleConversationResponse <$> createConnectConversation usr conn j - -createConnectConversation :: UserId -> Maybe ConnId -> Connect -> Galley ConversationResponse +createConnectConversation :: + UserId -> + Maybe ConnId -> + Connect -> + Galley ConversationResponse createConnectConversation usr conn j = do lusr <- qualifyLocal usr (x, y) <- toUUIDs usr (cRecipient j) diff --git a/services/galley/src/Galley/API/Internal.hs b/services/galley/src/Galley/API/Internal.hs index 65b8fd7a92e..fadfaaecdf0 100644 --- a/services/galley/src/Galley/API/Internal.hs +++ b/services/galley/src/Galley/API/Internal.hs @@ -79,6 +79,7 @@ import Wire.API.ErrorDescription (MissingLegalholdConsent) import Wire.API.Routes.MultiTablePaging (mtpHasMore, mtpPagingState, mtpResults) import Wire.API.Routes.MultiVerb (MultiVerb, RespondEmpty) import Wire.API.Routes.Public (ZOptConn, ZUser) +import Wire.API.Routes.Public.Galley (ConversationVerb) import qualified Wire.API.Team.Feature as Public data InternalApi routes = InternalApi @@ -175,7 +176,20 @@ data InternalApi routes = InternalApi :> ZOptConn :> "i" :> "user" - :> MultiVerb 'DELETE '[Servant.JSON] '[RespondEmpty 200 "Remove a user from Galley"] () + :> MultiVerb 'DELETE '[Servant.JSON] '[RespondEmpty 200 "Remove a user from Galley"] (), + -- This endpoint can lead to the following events being sent: + -- - ConvCreate event to self, if conversation did not exist before + -- - ConvConnect event to self, if other didn't join the connect conversation before + iConnect :: + routes + :- Summary "Create a connect conversation (deprecated)" + :> ZUser + :> ZOptConn + :> "i" + :> "conversations" + :> "connect" + :> ReqBody '[Servant.JSON] Connect + :> ConversationVerb } deriving (Generic) @@ -250,7 +264,8 @@ servantSitemap = iTeamFeatureStatusClassifiedDomainsGet = iGetTeamFeature @'Public.TeamFeatureClassifiedDomains Features.getClassifiedDomainsInternal, iTeamFeatureStatusConferenceCallingPut = iPutTeamFeature @'Public.TeamFeatureConferenceCalling Features.setConferenceCallingInternal, iTeamFeatureStatusConferenceCallingGet = iGetTeamFeature @'Public.TeamFeatureConferenceCalling Features.getConferenceCallingInternal, - iDeleteUser = rmUser + iDeleteUser = rmUser, + iConnect = Create.createConnectConversation } iGetTeamFeature :: @@ -290,14 +305,6 @@ sitemap = do .&. zauthConnId .&. jsonRequest @NewConvManaged - -- This endpoint can lead to the following events being sent: - -- - ConvCreate event to self, if conversation did not exist before - -- - ConvConnect event to self, if other didn't join the connect conversation before - post "/i/conversations/connect" (continue Create.createConnectConversationH) $ - zauthUserId - .&. opt zauthConnId - .&. jsonRequest @Connect - -- This endpoint can lead to the following events being sent: -- - MemberJoin event to you, if the conversation existed and had < 2 members before -- - MemberJoin event to other, if the conversation existed and only the other was member From 59a6f752b2e71ef384f46d0c6d2b19d83080218d Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Fri, 1 Oct 2021 10:21:59 +0200 Subject: [PATCH 09/27] Make recipient field of connect event qualified --- .../src/Wire/API/Event/Conversation.hs | 5 ++- .../Wire/API/Golden/Generated/Connect_user.hs | 12 +++++- .../Wire/API/Golden/Generated/Event_user.hs | 5 ++- services/brig/src/Brig/IO/Intra.hs | 17 ++++----- services/galley/src/Galley/API/Create.hs | 38 ++++++++++--------- services/galley/src/Galley/API/Util.hs | 2 +- services/galley/src/Galley/Data.hs | 12 +++--- services/galley/test/integration/API/Util.hs | 3 +- 8 files changed, 55 insertions(+), 39 deletions(-) diff --git a/libs/wire-api/src/Wire/API/Event/Conversation.hs b/libs/wire-api/src/Wire/API/Event/Conversation.hs index 0cc3da6702e..d681659225f 100644 --- a/libs/wire-api/src/Wire/API/Event/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Event/Conversation.hs @@ -352,7 +352,7 @@ instance ToSchema SimpleMember where .= (field "conversation_role" schema <|> pure roleNameWireAdmin) data Connect = Connect - { cRecipient :: UserId, + { cRecipient :: Qualified UserId, -- FUTUREWORK: As a follow-up from -- https://github.com/wireapp/wire-server/pull/1726, the message field can -- be removed from this event. @@ -370,7 +370,8 @@ instance ToSchema Connect where connectObjectSchema :: ObjectSchema SwaggerDoc Connect connectObjectSchema = Connect - <$> cRecipient .= field "recipient" schema + <$> cRecipient .= field "qualified_recipient" schema + <* (Just . qUnqualified . cRecipient) .= optField "recipient" Nothing schema <*> cMessage .= lax (field "message" (optWithDefault A.Null schema)) <*> cName .= lax (field "name" (optWithDefault A.Null schema)) <*> cEmail .= lax (field "email" (optWithDefault A.Null schema)) diff --git a/libs/wire-api/test/unit/Test/Wire/API/Golden/Generated/Connect_user.hs b/libs/wire-api/test/unit/Test/Wire/API/Golden/Generated/Connect_user.hs index ad3dcd121da..2e402120580 100644 --- a/libs/wire-api/test/unit/Test/Wire/API/Golden/Generated/Connect_user.hs +++ b/libs/wire-api/test/unit/Test/Wire/API/Golden/Generated/Connect_user.hs @@ -16,7 +16,9 @@ -- with this program. If not, see . module Test.Wire.API.Golden.Generated.Connect_user where +import Data.Domain import Data.Id (Id (Id)) +import Data.Qualified import qualified Data.UUID as UUID (fromString) import Imports (Maybe (Just, Nothing), fromJust) import Wire.API.Event.Conversation (Connect (..)) @@ -24,7 +26,10 @@ import Wire.API.Event.Conversation (Connect (..)) testObject_Connect_user_1 :: Connect testObject_Connect_user_1 = Connect - { cRecipient = Id (fromJust (UUID.fromString "00000002-0000-0001-0000-000400000004")), + { cRecipient = + Qualified + (Id (fromJust (UUID.fromString "00000002-0000-0001-0000-000400000004"))) + (Domain "foo.example.com"), cMessage = Just "E", cName = Just ".\128842]G", cEmail = Just "test email" @@ -33,7 +38,10 @@ testObject_Connect_user_1 = testObject_Connect_user_2 :: Connect testObject_Connect_user_2 = Connect - { cRecipient = Id (fromJust (UUID.fromString "00000005-0000-0007-0000-000200000008")), + { cRecipient = + Qualified + (Id (fromJust (UUID.fromString "00000005-0000-0007-0000-000200000008"))) + (Domain "bar.example.com"), cMessage = Nothing, cName = Nothing, cEmail = Nothing diff --git a/libs/wire-api/test/unit/Test/Wire/API/Golden/Generated/Event_user.hs b/libs/wire-api/test/unit/Test/Wire/API/Golden/Generated/Event_user.hs index 6f5e8a6c28a..bf7223ebb75 100644 --- a/libs/wire-api/test/unit/Test/Wire/API/Golden/Generated/Event_user.hs +++ b/libs/wire-api/test/unit/Test/Wire/API/Golden/Generated/Event_user.hs @@ -214,7 +214,10 @@ testObject_Event_user_10 = (read "1864-05-25 01:31:49.802 UTC") ( EdConnect ( Connect - { cRecipient = Id (fromJust (UUID.fromString "00000008-0000-0000-0000-000600000001")), + { cRecipient = + Qualified + (Id (fromJust (UUID.fromString "00000008-0000-0000-0000-000600000001"))) + (Domain "faraway.example.com"), cMessage = Just "L", cName = Just "fq", cEmail = Just "\992986" diff --git a/services/brig/src/Brig/IO/Intra.hs b/services/brig/src/Brig/IO/Intra.hs index afac8627dde..301beea269a 100644 --- a/services/brig/src/Brig/IO/Intra.hs +++ b/services/brig/src/Brig/IO/Intra.hs @@ -533,7 +533,7 @@ createSelfConv u = do . zUser u . expect2xx --- | Calls 'Galley.API.createConnectConversationH'. +-- | Calls 'Galley.API.Create.createConnectConversation'. createLocalConnectConv :: Local UserId -> Local UserId -> @@ -545,18 +545,17 @@ createLocalConnectConv from to cname conn = do logConnection (tUnqualified from) (qUntagged to) . remote "galley" . msg (val "Creating connect conversation") + let req = + path "/i/conversations/connect" + . zUser (tUnqualified from) + . maybe id (header "Z-Connection" . fromConnId) conn + . contentJson + . lbytes (encode $ Connect (qUntagged to) Nothing cname Nothing) + . expect2xx r <- galleyRequest POST req maybe (error "invalid conv id") return $ fromByteString $ getHeader' "Location" r - where - req = - path "/i/conversations/connect" - . zUser (tUnqualified from) - . maybe id (header "Z-Connection" . fromConnId) conn - . contentJson - . lbytes (encode $ Connect (tUnqualified to) Nothing cname Nothing) - . expect2xx createConnectConv :: Qualified UserId -> diff --git a/services/galley/src/Galley/API/Create.hs b/services/galley/src/Galley/API/Create.hs index f0aa268bde4..9c8eaacfcda 100644 --- a/services/galley/src/Galley/API/Create.hs +++ b/services/galley/src/Galley/API/Create.hs @@ -182,7 +182,7 @@ createOne2OneConversation zusr zcon (NewConvUnmanaged j) = do lusr <- qualifyLocal zusr let allUsers = newConvMembers lusr j other <- ensureOne (ulAll lusr allUsers) - when (unTagged lusr == other) $ + when (qUntagged lusr == other) $ throwM (invalidOp "Cannot create a 1-1 with yourself") mtid <- case newConvTeam j of Just ti @@ -198,7 +198,7 @@ createOne2OneConversation zusr zcon (NewConvUnmanaged j) = do foldQualified lusr (createLegacyOne2OneConversationUnchecked lusr zcon n mtid) - (createOne2OneConversationUnchecked lusr zcon n mtid . unTagged) + (createOne2OneConversationUnchecked lusr zcon n mtid . qUntagged) other where verifyMembership tid u = do @@ -206,12 +206,12 @@ createOne2OneConversation zusr zcon (NewConvUnmanaged j) = do when (isNothing membership) $ throwM noBindingTeamMembers checkBindingTeamPermissions lusr lother tid = do - zusrMembership <- Data.teamMember tid (lUnqualified lusr) + zusrMembership <- Data.teamMember tid (tUnqualified lusr) void $ permissionCheck CreateConversation zusrMembership Data.teamBinding tid >>= \case Just Binding -> do - verifyMembership tid (lUnqualified lusr) - verifyMembership tid (lUnqualified lother) + verifyMembership tid (tUnqualified lusr) + verifyMembership tid (tUnqualified lother) pure (Just tid) Just _ -> throwM nonBindingTeam Nothing -> throwM teamNotFound @@ -225,14 +225,14 @@ createLegacyOne2OneConversationUnchecked :: Galley ConversationResponse createLegacyOne2OneConversationUnchecked self zcon name mtid other = do lcnv <- localOne2OneConvId self other - mc <- Data.conversation (lUnqualified lcnv) + mc <- Data.conversation (tUnqualified lcnv) case mc of - Just c -> conversationExisted (lUnqualified self) c + Just c -> conversationExisted (tUnqualified self) c Nothing -> do - (x, y) <- toUUIDs (lUnqualified self) (lUnqualified other) + (x, y) <- toUUIDs (tUnqualified self) (tUnqualified other) c <- Data.createLegacyOne2OneConversation self x y name mtid - notifyCreatedConversation Nothing (lUnqualified self) (Just zcon) c - conversationCreated (lUnqualified self) c + notifyCreatedConversation Nothing (tUnqualified self) (Just zcon) c + conversationCreated (tUnqualified self) c createOne2OneConversationUnchecked :: Local UserId -> @@ -247,7 +247,7 @@ createOne2OneConversationUnchecked self zcon name mtid other = do self createOne2OneConversationLocally createOne2OneConversationRemotely - create (one2OneConvId (unTagged self) other) self zcon name mtid other + create (one2OneConvId (qUntagged self) other) self zcon name mtid other createOne2OneConversationLocally :: Local ConvId -> @@ -258,13 +258,13 @@ createOne2OneConversationLocally :: Qualified UserId -> Galley ConversationResponse createOne2OneConversationLocally lcnv self zcon name mtid other = do - mc <- Data.conversation (lUnqualified lcnv) + mc <- Data.conversation (tUnqualified lcnv) case mc of - Just c -> conversationExisted (lUnqualified self) c + Just c -> conversationExisted (tUnqualified self) c Nothing -> do c <- Data.createOne2OneConversation lcnv self other name mtid - notifyCreatedConversation Nothing (lUnqualified self) (Just zcon) c - conversationCreated (lUnqualified self) c + notifyCreatedConversation Nothing (tUnqualified self) (Just zcon) c + conversationCreated (tUnqualified self) c createOne2OneConversationRemotely :: Remote ConvId -> @@ -283,8 +283,12 @@ createConnectConversation :: Connect -> Galley ConversationResponse createConnectConversation usr conn j = do + localDomain <- viewFederationDomain + let qrecipient = cRecipient j + when (qDomain qrecipient /= localDomain) $ + throwM federationNotImplemented lusr <- qualifyLocal usr - (x, y) <- toUUIDs usr (cRecipient j) + (x, y) <- toUUIDs usr (qUnqualified qrecipient) n <- rangeCheckedMaybe (cName j) conv <- Data.conversation (Data.one2OneConvId x y) maybe (create lusr x y n) (update n) conv @@ -388,7 +392,7 @@ notifyCreatedConversation dtime usr conn c = do localOne2OneConvId :: Local UserId -> Local UserId -> Galley (Local ConvId) localOne2OneConvId self other = do - (x, y) <- toUUIDs (lUnqualified self) (lUnqualified other) + (x, y) <- toUUIDs (tUnqualified self) (tUnqualified other) pure . qualifyAs self $ Data.one2OneConvId x y toUUIDs :: UserId -> UserId -> Galley (U.UUID U.V4, U.UUID U.V4) diff --git a/services/galley/src/Galley/API/Util.hs b/services/galley/src/Galley/API/Util.hs index 0586568e9e0..f12365ae722 100644 --- a/services/galley/src/Galley/API/Util.hs +++ b/services/galley/src/Galley/API/Util.hs @@ -109,7 +109,7 @@ ensureConnectedOrSameTeam (Qualified u domain) uids = do ensureConnected :: Local UserId -> UserList UserId -> Galley () ensureConnected self others = do -- FUTUREWORK(federation, #1262): check remote connections - ensureConnectedToLocals (lUnqualified self) (ulLocals others) + ensureConnectedToLocals (tUnqualified self) (ulLocals others) ensureConnectedToLocals :: UserId -> [UserId] -> Galley () ensureConnectedToLocals _ [] = pure () diff --git a/services/galley/src/Galley/Data.hs b/services/galley/src/Galley/Data.hs index 3e24895b8ce..5cf0911d12a 100644 --- a/services/galley/src/Galley/Data.hs +++ b/services/galley/src/Galley/Data.hs @@ -705,7 +705,7 @@ createLegacyOne2OneConversation loc a b name ti = do createOne2OneConversation lconv (qualifyAs loc a') - (unTagged (qualifyAs loc b')) + (qUntagged (qualifyAs loc b')) name ti @@ -719,14 +719,14 @@ createOne2OneConversation :: m Conversation createOne2OneConversation lconv self other name mtid = do retry x5 $ case mtid of - Nothing -> write Cql.insertConv (params Quorum (lUnqualified lconv, One2OneConv, lUnqualified self, privateOnly, privateRole, fromRange <$> name, Nothing, Nothing, Nothing)) + Nothing -> write Cql.insertConv (params Quorum (tUnqualified lconv, One2OneConv, tUnqualified self, privateOnly, privateRole, fromRange <$> name, Nothing, Nothing, Nothing)) Just tid -> batch $ do setType BatchLogged setConsistency Quorum - addPrepQuery Cql.insertConv (lUnqualified lconv, One2OneConv, lUnqualified self, privateOnly, privateRole, fromRange <$> name, Just tid, Nothing, Nothing) - addPrepQuery Cql.insertTeamConv (tid, lUnqualified lconv, False) - (lmems, rmems) <- addMembers lconv (toUserList self [unTagged self, other]) - pure $ newConv (lUnqualified lconv) One2OneConv (lUnqualified self) lmems rmems [PrivateAccess] privateRole name mtid Nothing Nothing + addPrepQuery Cql.insertConv (tUnqualified lconv, One2OneConv, tUnqualified self, privateOnly, privateRole, fromRange <$> name, Just tid, Nothing, Nothing) + addPrepQuery Cql.insertTeamConv (tid, tUnqualified lconv, False) + (lmems, rmems) <- addMembers lconv (toUserList self [qUntagged self, other]) + pure $ newConv (tUnqualified lconv) One2OneConv (tUnqualified self) lmems rmems [PrivateAccess] privateRole name mtid Nothing Nothing updateConversation :: MonadClient m => ConvId -> Range 1 256 Text -> m () updateConversation cid name = retry x5 $ write Cql.updateConvName (params Quorum (fromRange name, cid)) diff --git a/services/galley/test/integration/API/Util.hs b/services/galley/test/integration/API/Util.hs index ec3bbcc83fc..140011ee079 100644 --- a/services/galley/test/integration/API/Util.hs +++ b/services/galley/test/integration/API/Util.hs @@ -594,6 +594,7 @@ postO2OConv u1 u2 n = do postConnectConv :: UserId -> UserId -> Text -> Text -> Maybe Text -> TestM ResponseLBS postConnectConv a b name msg email = do + qb <- Qualified <$> pure b <*> viewFederationDomain g <- view tsGalley post $ g @@ -601,7 +602,7 @@ postConnectConv a b name msg email = do . zUser a . zConn "conn" . zType "access" - . json (Connect b (Just msg) (Just name) email) + . json (Connect qb (Just msg) (Just name) email) putConvAccept :: UserId -> ConvId -> TestM ResponseLBS putConvAccept invited cid = do From 342884458f7dafd63af08ca61d3ee86bb7fad365 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Fri, 1 Oct 2021 10:33:02 +0200 Subject: [PATCH 10/27] Extract function to create legacy connect conv --- services/galley/src/Galley/API/Create.hs | 49 ++++++++++++++++-------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/services/galley/src/Galley/API/Create.hs b/services/galley/src/Galley/API/Create.hs index 9c8eaacfcda..7a944cd9a5d 100644 --- a/services/galley/src/Galley/API/Create.hs +++ b/services/galley/src/Galley/API/Create.hs @@ -283,38 +283,54 @@ createConnectConversation :: Connect -> Galley ConversationResponse createConnectConversation usr conn j = do - localDomain <- viewFederationDomain - let qrecipient = cRecipient j - when (qDomain qrecipient /= localDomain) $ - throwM federationNotImplemented lusr <- qualifyLocal usr - (x, y) <- toUUIDs usr (qUnqualified qrecipient) + foldQualified + lusr + (\lrcpt -> createLegacyConnectConversation lusr conn lrcpt j) + (createConnectConversationWithRemote lusr conn) + (cRecipient j) + +createConnectConversationWithRemote :: + Local UserId -> + Maybe ConnId -> + Remote UserId -> + Galley ConversationResponse +createConnectConversationWithRemote _ _ _ = + throwM federationNotImplemented + +createLegacyConnectConversation :: + Local UserId -> + Maybe ConnId -> + Local UserId -> + Connect -> + Galley ConversationResponse +createLegacyConnectConversation lusr conn lrecipient j = do + (x, y) <- toUUIDs (tUnqualified lusr) (tUnqualified lrecipient) n <- rangeCheckedMaybe (cName j) conv <- Data.conversation (Data.one2OneConvId x y) - maybe (create lusr x y n) (update n) conv + maybe (create x y n) (update n) conv where - create lusr x y n = do + create x y n = do c <- Data.createConnectConversation lusr x y n now <- liftIO getCurrentTime let lcid = qualifyAs lusr (Data.convId c) e = Event ConvConnect (qUntagged lcid) (qUntagged lusr) now (EdConnect j) - notifyCreatedConversation Nothing usr conn c - for_ (newPushLocal ListComplete usr (ConvEvent e) (recipient <$> Data.convLocalMembers c)) $ \p -> + notifyCreatedConversation Nothing (tUnqualified lusr) conn c + for_ (newPushLocal ListComplete (tUnqualified lusr) (ConvEvent e) (recipient <$> Data.convLocalMembers c)) $ \p -> push1 $ p & pushRoute .~ RouteDirect & pushConn .~ conn - conversationCreated usr c + conversationCreated (tUnqualified lusr) c update n conv = do let mems = Data.convLocalMembers conv - in conversationExisted usr + in conversationExisted (tUnqualified lusr) =<< if - | usr `isMember` mems -> + | (tUnqualified lusr) `isMember` mems -> -- we already were in the conversation, maybe also other connect n conv | otherwise -> do lcid <- qualifyLocal (Data.convId conv) - lusr <- qualifyLocal usr mm <- Data.addMember lcid lusr let conv' = conv @@ -326,7 +342,7 @@ createConnectConversation usr conn j = do connect n conv' else do -- we were not in the conversation, but someone else - conv'' <- acceptOne2One usr conv' conn + conv'' <- acceptOne2One (tUnqualified lusr) conv' conn if Data.convType conv'' == ConnectConv then connect n conv'' else return conv'' @@ -334,15 +350,14 @@ createConnectConversation usr conn j = do | Data.convType conv == ConnectConv = do localDomain <- viewFederationDomain let qconv = Qualified (Data.convId conv) localDomain - qusr = Qualified usr localDomain n' <- case n of Just x -> do Data.updateConversation (Data.convId conv) x return . Just $ fromRange x Nothing -> return $ Data.convName conv t <- liftIO getCurrentTime - let e = Event ConvConnect qconv qusr t (EdConnect j) - for_ (newPushLocal ListComplete usr (ConvEvent e) (recipient <$> Data.convLocalMembers conv)) $ \p -> + let e = Event ConvConnect qconv (qUntagged lusr) t (EdConnect j) + for_ (newPushLocal ListComplete (tUnqualified lusr) (ConvEvent e) (recipient <$> Data.convLocalMembers conv)) $ \p -> push1 $ p & pushRoute .~ RouteDirect From 42e1fe9a9d92ebbbc4a26bf5460d69954a303132 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Mon, 4 Oct 2021 16:12:01 +0200 Subject: [PATCH 11/27] Add tests for the conversation ID algorithm --- services/galley/galley.cabal | 8 ++- services/galley/package.yaml | 2 +- services/galley/src/Galley/API/One2One.hs | 6 ++- services/galley/test/unit/Main.hs | 2 + .../test/unit/Test/Galley/API/One2One.hs | 51 +++++++++++++++++++ 5 files changed, 66 insertions(+), 3 deletions(-) create mode 100644 services/galley/test/unit/Test/Galley/API/One2One.hs diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal index da44789be72..9ed22c5b6c8 100644 --- a/services/galley/galley.cabal +++ b/services/galley/galley.cabal @@ -4,7 +4,7 @@ cabal-version: 1.12 -- -- see: https://github.com/sol/hpack -- --- hash: 7ca3c61253dfbb1c69f3cf336a7bf166a1b50d9f92bc327012b395045112d7b7 +-- hash: 10735fce03e5965b253e8d0360c4bfca3db877730b220367acd5d14083af69c5 name: galley version: 0.83.0 @@ -176,6 +176,7 @@ executable galley , base , case-insensitive , extended + , extra >=1.3 , galley , galley-types , imports @@ -239,6 +240,7 @@ executable galley-integration , errors , exceptions , extended + , extra >=1.3 , galley , galley-types , gundeck-types @@ -315,6 +317,7 @@ executable galley-migrate-data , containers , exceptions , extended + , extra >=1.3 , galley-types , imports , lens @@ -382,6 +385,7 @@ executable galley-schema , case-insensitive , cassandra-util , extended + , extra >=1.3 , imports , optparse-applicative , raw-strings-qq >=1.0 @@ -403,6 +407,7 @@ test-suite galley-types-tests other-modules: Test.Galley.API Test.Galley.API.Message + Test.Galley.API.One2One Test.Galley.Intra.User Test.Galley.Mapping Test.Galley.Roundtrip @@ -417,6 +422,7 @@ test-suite galley-types-tests , case-insensitive , containers , extended + , extra >=1.3 , galley , galley-types , http-types diff --git a/services/galley/package.yaml b/services/galley/package.yaml index a1338730634..f993d78d0d8 100644 --- a/services/galley/package.yaml +++ b/services/galley/package.yaml @@ -13,6 +13,7 @@ dependencies: - imports - case-insensitive - extended +- extra >=1.3 - safe >=0.3 - ssl-util - raw-strings-qq >=1.0 @@ -45,7 +46,6 @@ library: - enclosed-exceptions >=1.0 - errors >=2.0 - exceptions >=0.4 - - extra >=1.3 - galley-types >=0.65.0 - gundeck-types >=1.35.2 - HsOpenSSL >=0.11 diff --git a/services/galley/src/Galley/API/One2One.hs b/services/galley/src/Galley/API/One2One.hs index daa542a6983..3bfcfd26ddb 100644 --- a/services/galley/src/Galley/API/One2One.hs +++ b/services/galley/src/Galley/API/One2One.hs @@ -102,11 +102,15 @@ one2OneConvId a b = case compareDomains a b of result = U.toUUID . U.mk @U.V5 . fromMaybe UUID.nil + -- fromByteString only returns 'Nothing' when the input is not + -- exactly 16 bytes long, here this should not be a case since + -- 'hash' is supposed to return atleast 16 bytes and we use 'B.take + -- 16' to truncate it . UUID.fromByteString . L.fromStrict . B.take 16 $ x domain - | (fromMaybe 0 (atMay (B.unpack x) 16)) .&. 0x80 == 0 = qDomain a + | fromMaybe 0 (atMay (B.unpack x) 16) .&. 0x80 == 0 = qDomain a | otherwise = qDomain b in Qualified (Id result) domain diff --git a/services/galley/test/unit/Main.hs b/services/galley/test/unit/Main.hs index c0a1dbcdd31..bfd608d4db7 100644 --- a/services/galley/test/unit/Main.hs +++ b/services/galley/test/unit/Main.hs @@ -23,6 +23,7 @@ where import Imports import qualified Test.Galley.API import qualified Test.Galley.API.Message +import qualified Test.Galley.API.One2One import qualified Test.Galley.Intra.User import qualified Test.Galley.Mapping import qualified Test.Galley.Roundtrip @@ -34,6 +35,7 @@ main = =<< sequence [ pure Test.Galley.API.tests, pure Test.Galley.API.Message.tests, + pure Test.Galley.API.One2One.tests, pure Test.Galley.Intra.User.tests, pure Test.Galley.Mapping.tests, Test.Galley.Roundtrip.tests diff --git a/services/galley/test/unit/Test/Galley/API/One2One.hs b/services/galley/test/unit/Test/Galley/API/One2One.hs new file mode 100644 index 00000000000..13201e77037 --- /dev/null +++ b/services/galley/test/unit/Test/Galley/API/One2One.hs @@ -0,0 +1,51 @@ +{-# LANGUAGE NumericUnderscores #-} +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2021 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 . +{-# OPTIONS_GHC -Wno-deferred-type-errors #-} + +-- | Tests for one-to-one conversations +module Test.Galley.API.One2One where + +import Data.Id +import Data.List.Extra +import Data.Qualified +import Galley.API.One2One (one2OneConvId) +import Imports +import Test.Tasty +import Test.Tasty.HUnit (Assertion, testCase, (@?=)) +import Test.Tasty.QuickCheck + +tests :: TestTree +tests = + testGroup + "one2OneConvId" + [ testProperty "symmetry" one2OneConvIdSymmetry, + testCase "non-collision" one2OneConvIdNonCollision + ] + +one2OneConvIdSymmetry :: Qualified UserId -> Qualified UserId -> Property +one2OneConvIdSymmetry quid1 quid2 = one2OneConvId quid1 quid2 === one2OneConvId quid2 quid1 + +-- | Make sure that we never get the same conversation ID for a pair of +-- (assumingly) distinct qualified user IDs +one2OneConvIdNonCollision :: Assertion +one2OneConvIdNonCollision = do + let len = 10_000 + -- A generator of lists of length 'len' of qualified user ID pairs + let gen = vectorOf len arbitrary + quids <- head <$> sample' gen + anySame (fmap (uncurry one2OneConvId) quids) @?= False From 7c28551ef7236c48c57f9cde574bd547cd0d69dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Tue, 5 Oct 2021 17:06:37 +0200 Subject: [PATCH 12/27] write internal with stubs for data functions --- libs/galley-types/galley-types.cabal | 3 +- .../src/Galley/Types/Conversations/Remote.hs | 64 +++++++++++++++++++ services/galley/galley.cabal | 3 +- services/galley/package.yaml | 1 + services/galley/src/Galley/API/Create.hs | 1 + services/galley/src/Galley/API/Internal.hs | 16 ++++- services/galley/src/Galley/API/One2One.hs | 48 +++++++++++++- services/galley/src/Galley/Data.hs | 20 ++++++ 8 files changed, 151 insertions(+), 5 deletions(-) create mode 100644 libs/galley-types/src/Galley/Types/Conversations/Remote.hs diff --git a/libs/galley-types/galley-types.cabal b/libs/galley-types/galley-types.cabal index 891555cac72..2b542d9c505 100644 --- a/libs/galley-types/galley-types.cabal +++ b/libs/galley-types/galley-types.cabal @@ -4,7 +4,7 @@ cabal-version: 1.12 -- -- see: https://github.com/sol/hpack -- --- hash: 8d07ea070b6384ec247f4473abb198bbb9639f72543920cbe46f561df96963ca +-- hash: cc9f3ca760dba01b0959dd075d6c7b7d90a9ed8551322671bba7d7c9ca8fb64b name: galley-types version: 0.81.0 @@ -23,6 +23,7 @@ library Galley.Types.Bot Galley.Types.Bot.Service Galley.Types.Conversations.Members + Galley.Types.Conversations.Remote Galley.Types.Conversations.Roles Galley.Types.Teams Galley.Types.Teams.Intra diff --git a/libs/galley-types/src/Galley/Types/Conversations/Remote.hs b/libs/galley-types/src/Galley/Types/Conversations/Remote.hs new file mode 100644 index 00000000000..7b8f06cbc4b --- /dev/null +++ b/libs/galley-types/src/Galley/Types/Conversations/Remote.hs @@ -0,0 +1,64 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2021 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 . +-- TODO: Rename to Galley.Types.Conversations.Intra +module Galley.Types.Conversations.Remote + ( DesiredMembership (..), + UpsertOne2OneConversationRequest (..), + UpsertOne2OneConversationResponse (..), + ) +where + +import Data.Aeson (FromJSON) +import Data.Aeson.Types (ToJSON) +import Data.Id (ConvId, UserId) +import Data.Qualified (Local, Qualified, Remote) +import Imports + +data DesiredMembership = Included | Excluded + deriving (Show, Generic) + +-- TODO: write cusom instances with roundtrip tests +instance ToJSON DesiredMembership + +-- TODO: write cusom instances with roundtrip tests +instance FromJSON DesiredMembership + +data UpsertOne2OneConversationRequest = UpsertOne2OneConversationRequest + { uooSelf :: Local UserId, + uooSelfDesiredMembership :: DesiredMembership, + uooOther :: Remote UserId, + uooOtherDesiredMembership :: DesiredMembership, + uooConvId :: Maybe (Qualified ConvId) + } + deriving (Show, Generic) + +-- TODO: write cusom instances with roundtrip tests +instance ToJSON UpsertOne2OneConversationRequest + +-- TODO: write cusom instances with roundtrip tests +instance FromJSON UpsertOne2OneConversationRequest + +data UpsertOne2OneConversationResponse = UpsertOne2OneConversationResponse + { uuorConvId :: Maybe (Qualified ConvId) + } + deriving (Show, Generic) + +-- TODO: write cusom instances with roundtrip tests +instance ToJSON UpsertOne2OneConversationResponse + +-- TODO: write cusom instances with roundtrip tests +instance FromJSON UpsertOne2OneConversationResponse diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal index 9ed22c5b6c8..78022187a1f 100644 --- a/services/galley/galley.cabal +++ b/services/galley/galley.cabal @@ -4,7 +4,7 @@ cabal-version: 1.12 -- -- see: https://github.com/sol/hpack -- --- hash: 10735fce03e5965b253e8d0360c4bfca3db877730b220367acd5d14083af69c5 +-- hash: b493b5406526fa471b5bccba17b8a264edf4ce67115987c8666e729ec24c2a2c name: galley version: 0.83.0 @@ -143,6 +143,7 @@ library , swagger2 , tagged , text >=0.11 + , these , time >=1.4 , tinylog >=0.10 , tls >=1.3.10 diff --git a/services/galley/package.yaml b/services/galley/package.yaml index f993d78d0d8..cbf5fd04cb5 100644 --- a/services/galley/package.yaml +++ b/services/galley/package.yaml @@ -82,6 +82,7 @@ library: - swagger >=0.1 - swagger2 - text >=0.11 + - these - time >=1.4 - tinylog >=0.10 - tls >=1.3.10 diff --git a/services/galley/src/Galley/API/Create.hs b/services/galley/src/Galley/API/Create.hs index 7a944cd9a5d..919d5b4a46d 100644 --- a/services/galley/src/Galley/API/Create.hs +++ b/services/galley/src/Galley/API/Create.hs @@ -290,6 +290,7 @@ createConnectConversation usr conn j = do (createConnectConversationWithRemote lusr conn) (cRecipient j) +-- TODO: remove createConnectConversationWithRemote :: Local UserId -> Maybe ConnId -> diff --git a/services/galley/src/Galley/API/Internal.hs b/services/galley/src/Galley/API/Internal.hs index fadfaaecdf0..50721130624 100644 --- a/services/galley/src/Galley/API/Internal.hs +++ b/services/galley/src/Galley/API/Internal.hs @@ -43,6 +43,7 @@ import qualified Galley.API.CustomBackend as CustomBackend import Galley.API.Error (throwErrorDescriptionType) import Galley.API.LegalHold (getTeamLegalholdWhitelistedH, setTeamLegalholdWhitelistedH, unsetTeamLegalholdWhitelistedH) import Galley.API.LegalHold.Conflicts (guardLegalholdPolicyConflicts) +import qualified Galley.API.One2One as One2One import qualified Galley.API.Query as Query import Galley.API.Teams (uncheckedDeleteTeamMember) import qualified Galley.API.Teams as Teams @@ -57,6 +58,7 @@ import qualified Galley.Queue as Q import Galley.Types import Galley.Types.Bot (AddBot, RemoveBot) import Galley.Types.Bot.Service +import Galley.Types.Conversations.Remote (UpsertOne2OneConversationRequest (..), UpsertOne2OneConversationResponse (..)) import Galley.Types.Teams hiding (MemberLeave) import Galley.Types.Teams.Intra import Galley.Types.Teams.SearchVisibility @@ -189,7 +191,16 @@ data InternalApi routes = InternalApi :> "conversations" :> "connect" :> ReqBody '[Servant.JSON] Connect - :> ConversationVerb + :> ConversationVerb, + iUpsertOne2OneConversation :: + routes + :- Summary "Create or Update a connect or one2one conversation." + :> "i" + :> "conversations" + :> "one2one" + :> "upsert" + :> ReqBody '[Servant.JSON] UpsertOne2OneConversationRequest + :> Post '[Servant.JSON] UpsertOne2OneConversationResponse } deriving (Generic) @@ -265,7 +276,8 @@ servantSitemap = iTeamFeatureStatusConferenceCallingPut = iPutTeamFeature @'Public.TeamFeatureConferenceCalling Features.setConferenceCallingInternal, iTeamFeatureStatusConferenceCallingGet = iGetTeamFeature @'Public.TeamFeatureConferenceCalling Features.getConferenceCallingInternal, iDeleteUser = rmUser, - iConnect = Create.createConnectConversation + iConnect = Create.createConnectConversation, + iUpsertOne2OneConversation = One2One.iUpsertOne2OneConversation } iGetTeamFeature :: diff --git a/services/galley/src/Galley/API/One2One.hs b/services/galley/src/Galley/API/One2One.hs index 3bfcfd26ddb..f014a2e1f2c 100644 --- a/services/galley/src/Galley/API/One2One.hs +++ b/services/galley/src/Galley/API/One2One.hs @@ -14,8 +14,13 @@ -- -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . +{-# LANGUAGE RecordWildCards #-} -module Galley.API.One2One (one2OneConvId) where +module Galley.API.One2One + ( one2OneConvId, + iUpsertOne2OneConversation, + ) +where import Control.Error (atMay) import qualified Crypto.Hash as Crypto @@ -26,9 +31,14 @@ import Data.ByteString.Conversion import qualified Data.ByteString.Lazy as L import Data.Id import Data.Qualified +import Data.Tagged (Tagged (unTagged)) +import Data.These (These (..)) import Data.UUID (UUID) import qualified Data.UUID as UUID import qualified Data.UUID.Tagged as U +import Galley.App (Galley) +import qualified Galley.Data as Data +import Galley.Types.Conversations.Remote (DesiredMembership (..), UpsertOne2OneConversationRequest (..), UpsertOne2OneConversationResponse (..)) import Imports -- | The hash function used to obtain the 1-1 conversation ID for a pair of users. @@ -114,3 +124,39 @@ one2OneConvId a b = case compareDomains a b of | fromMaybe 0 (atMay (B.unpack x) 16) .&. 0x80 == 0 = qDomain a | otherwise = qDomain b in Qualified (Id result) domain + +iUpsertOne2OneConversation :: UpsertOne2OneConversationRequest -> Galley UpsertOne2OneConversationResponse +iUpsertOne2OneConversation UpsertOne2OneConversationRequest {..} = do + let convId = fromMaybe (one2OneConvId (unTagged uooSelf) (unTagged uooOther)) uooConvId + + let dolocal :: Local ConvId -> Galley (Maybe (Qualified ConvId)) + dolocal lconvId = do + let mMembers = case (uooSelfDesiredMembership, uooOtherDesiredMembership) of + (Excluded, Excluded) -> Nothing + (Included, Excluded) -> Just (This uooSelf) + (Excluded, Included) -> Just (That uooOther) + (Included, Included) -> Just (These uooSelf uooOther) + + mbConv <- Data.conversation (lUnqualified lconvId) + case mbConv of + Nothing -> do + case mMembers of + Nothing -> + pure Nothing + Just members -> + Just . unTagged <$> Data.createOne2OneConversationRemote lconvId uooSelf members + Just _conv -> + case mMembers of + Nothing -> pure Nothing -- TODO: delete conversation? + Just members -> Just . unTagged <$> Data.updateOne2OneConversationRemoteMembers lconvId members + + doremote :: Remote ConvId -> Galley (Maybe (Qualified ConvId)) + doremote rconvId = do + case uooSelfDesiredMembership of + Excluded -> + Data.removeLocalMembersFromRemoteConv (unTagged rconvId) [lUnqualified uooSelf] + Included -> + Data.addLocalMembersToRemoteConv (unTagged rconvId) [lUnqualified uooSelf] + pure (Just . unTagged $ rconvId) + + UpsertOne2OneConversationResponse <$> foldQualified uooSelf dolocal doremote convId diff --git a/services/galley/src/Galley/Data.hs b/services/galley/src/Galley/Data.hs index 5cf0911d12a..9503a4219ba 100644 --- a/services/galley/src/Galley/Data.hs +++ b/services/galley/src/Galley/Data.hs @@ -68,6 +68,8 @@ module Galley.Data conversationMeta, conversationsRemote, createConnectConversation, + createOne2OneConversationRemote, + updateOne2OneConversationRemoteMembers, createConversation, createLegacyOne2OneConversation, createOne2OneConversation, @@ -144,6 +146,7 @@ import qualified Data.Monoid import Data.Qualified import Data.Range import qualified Data.Set as Set +import Data.These (These (..)) import qualified Data.UUID.Tagged as U import Data.UUID.V4 (nextRandom) import Galley.App @@ -689,6 +692,23 @@ createConnectConversation loc a b name = do (lmems, rmems) <- addMembers lconv (UserList [a'] []) pure $ newConv conv ConnectConv a' lmems rmems [PrivateAccess] privateRole name Nothing Nothing Nothing +createOne2OneConversationRemote :: + MonadClient m => + Local ConvId -> + Local UserId -> + These (Local UserId) (Remote UserId) -> + m (Local ConvId) +createOne2OneConversationRemote _lconvId _creator _members = + error "Create conversation. If 'These' members a One2One conversation, otherwise a ConnectConversation" + +updateOne2OneConversationRemoteMembers :: + MonadClient m => + Local ConvId -> + These (Local UserId) (Remote UserId) -> + m (Local ConvId) +updateOne2OneConversationRemoteMembers = + error "Update members and conversation type." + createLegacyOne2OneConversation :: MonadClient m => Local x -> From cf54c8ddd2260c759ce97fbefd757afc351836ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Wed, 6 Oct 2021 14:48:46 +0200 Subject: [PATCH 13/27] Implement a function for creating and updating a 1-1 remote conversation - The function is Galley.API.One2One.iUpsertOne2OneConversation --- .../src/Galley/Types/Conversations/Remote.hs | 22 +++++-- services/galley/src/Galley/API/One2One.hs | 64 +++++++++++-------- services/galley/src/Galley/Data.hs | 26 +++----- 3 files changed, 64 insertions(+), 48 deletions(-) diff --git a/libs/galley-types/src/Galley/Types/Conversations/Remote.hs b/libs/galley-types/src/Galley/Types/Conversations/Remote.hs index 7b8f06cbc4b..5f06e32decb 100644 --- a/libs/galley-types/src/Galley/Types/Conversations/Remote.hs +++ b/libs/galley-types/src/Galley/Types/Conversations/Remote.hs @@ -17,6 +17,7 @@ -- TODO: Rename to Galley.Types.Conversations.Intra module Galley.Types.Conversations.Remote ( DesiredMembership (..), + Actor (..), UpsertOne2OneConversationRequest (..), UpsertOne2OneConversationResponse (..), ) @@ -37,11 +38,19 @@ instance ToJSON DesiredMembership -- TODO: write cusom instances with roundtrip tests instance FromJSON DesiredMembership +data Actor = LocalActor | RemoteActor deriving (Show, Eq, Generic) + +-- TODO: write cusom instances with roundtrip tests +instance ToJSON Actor + +-- TODO: write cusom instances with roundtrip tests +instance FromJSON Actor + data UpsertOne2OneConversationRequest = UpsertOne2OneConversationRequest - { uooSelf :: Local UserId, - uooSelfDesiredMembership :: DesiredMembership, - uooOther :: Remote UserId, - uooOtherDesiredMembership :: DesiredMembership, + { uooLocalUser :: Local UserId, + uooRemoteUser :: Remote UserId, + uooActor :: Actor, + uooActorDesiredMembership :: DesiredMembership, uooConvId :: Maybe (Qualified ConvId) } deriving (Show, Generic) @@ -53,7 +62,10 @@ instance ToJSON UpsertOne2OneConversationRequest instance FromJSON UpsertOne2OneConversationRequest data UpsertOne2OneConversationResponse = UpsertOne2OneConversationResponse - { uuorConvId :: Maybe (Qualified ConvId) + { -- | The Nothing value here indicated that there an impossible request was + -- received, e.g., requesting to remove a remote user when the actor is a + -- local user. + uuorConvId :: Maybe (Qualified ConvId) } deriving (Show, Generic) diff --git a/services/galley/src/Galley/API/One2One.hs b/services/galley/src/Galley/API/One2One.hs index f014a2e1f2c..f29a2565883 100644 --- a/services/galley/src/Galley/API/One2One.hs +++ b/services/galley/src/Galley/API/One2One.hs @@ -32,13 +32,13 @@ import qualified Data.ByteString.Lazy as L import Data.Id import Data.Qualified import Data.Tagged (Tagged (unTagged)) -import Data.These (These (..)) import Data.UUID (UUID) import qualified Data.UUID as UUID import qualified Data.UUID.Tagged as U import Galley.App (Galley) import qualified Galley.Data as Data -import Galley.Types.Conversations.Remote (DesiredMembership (..), UpsertOne2OneConversationRequest (..), UpsertOne2OneConversationResponse (..)) +import Galley.Types.Conversations.Remote (Actor (..), DesiredMembership (..), UpsertOne2OneConversationRequest (..), UpsertOne2OneConversationResponse (..)) +import Galley.Types.UserList (UserList (..)) import Imports -- | The hash function used to obtain the 1-1 conversation ID for a pair of users. @@ -127,36 +127,46 @@ one2OneConvId a b = case compareDomains a b of iUpsertOne2OneConversation :: UpsertOne2OneConversationRequest -> Galley UpsertOne2OneConversationResponse iUpsertOne2OneConversation UpsertOne2OneConversationRequest {..} = do - let convId = fromMaybe (one2OneConvId (unTagged uooSelf) (unTagged uooOther)) uooConvId + let convId = fromMaybe (one2OneConvId (unTagged uooLocalUser) (unTagged uooRemoteUser)) uooConvId let dolocal :: Local ConvId -> Galley (Maybe (Qualified ConvId)) dolocal lconvId = do - let mMembers = case (uooSelfDesiredMembership, uooOtherDesiredMembership) of - (Excluded, Excluded) -> Nothing - (Included, Excluded) -> Just (This uooSelf) - (Excluded, Included) -> Just (That uooOther) - (Included, Included) -> Just (These uooSelf uooOther) - mbConv <- Data.conversation (lUnqualified lconvId) case mbConv of Nothing -> do - case mMembers of - Nothing -> - pure Nothing - Just members -> - Just . unTagged <$> Data.createOne2OneConversationRemote lconvId uooSelf members - Just _conv -> - case mMembers of - Nothing -> pure Nothing -- TODO: delete conversation? - Just members -> Just . unTagged <$> Data.updateOne2OneConversationRemoteMembers lconvId members - + let members = + case (uooActor, uooActorDesiredMembership) of + (LocalActor, Included) -> UserList [lUnqualified uooLocalUser] [] + (LocalActor, Excluded) -> UserList [] [] + (RemoteActor, Included) -> UserList [] [uooRemoteUser] + (RemoteActor, Excluded) -> UserList [] [] + case members of + UserList [] [] -> pure Nothing + _ -> do + Data.createConnectConversationWithRemote lconvId uooLocalUser members + pure . Just $ convId + Just conv -> do + case (uooActor, uooActorDesiredMembership) of + (LocalActor, Included) -> do + void $ Data.addMember lconvId uooLocalUser + unless (null (Data.convRemoteMembers conv)) $ + Data.acceptConnect (lUnqualified lconvId) + (LocalActor, Excluded) -> Data.removeMember (lUnqualified uooLocalUser) (lUnqualified lconvId) + (RemoteActor, Included) -> do + void $ Data.addMembers lconvId (UserList [] [uooRemoteUser]) + unless (null (Data.convLocalMembers conv)) $ + Data.acceptConnect (lUnqualified lconvId) + (RemoteActor, Excluded) -> Data.removeRemoteMembersFromLocalConv (lUnqualified lconvId) (pure uooRemoteUser) + pure . Just . unTagged $ lconvId doremote :: Remote ConvId -> Galley (Maybe (Qualified ConvId)) - doremote rconvId = do - case uooSelfDesiredMembership of - Excluded -> - Data.removeLocalMembersFromRemoteConv (unTagged rconvId) [lUnqualified uooSelf] - Included -> - Data.addLocalMembersToRemoteConv (unTagged rconvId) [lUnqualified uooSelf] - pure (Just . unTagged $ rconvId) + doremote rconvId = + case (uooActor, uooActorDesiredMembership) of + (LocalActor, Included) -> do + Data.addLocalMembersToRemoteConv (unTagged rconvId) [lUnqualified uooLocalUser] + pure . Just . unTagged $ rconvId + (LocalActor, Excluded) -> do + Data.removeLocalMembersFromRemoteConv (unTagged rconvId) [lUnqualified uooLocalUser] + pure . Just . unTagged $ rconvId + (RemoteActor, _) -> pure Nothing - UpsertOne2OneConversationResponse <$> foldQualified uooSelf dolocal doremote convId + UpsertOne2OneConversationResponse <$> foldQualified uooLocalUser dolocal doremote convId diff --git a/services/galley/src/Galley/Data.hs b/services/galley/src/Galley/Data.hs index 9503a4219ba..6160c09a17c 100644 --- a/services/galley/src/Galley/Data.hs +++ b/services/galley/src/Galley/Data.hs @@ -68,8 +68,7 @@ module Galley.Data conversationMeta, conversationsRemote, createConnectConversation, - createOne2OneConversationRemote, - updateOne2OneConversationRemoteMembers, + createConnectConversationWithRemote, createConversation, createLegacyOne2OneConversation, createOne2OneConversation, @@ -146,7 +145,6 @@ import qualified Data.Monoid import Data.Qualified import Data.Range import qualified Data.Set as Set -import Data.These (These (..)) import qualified Data.UUID.Tagged as U import Data.UUID.V4 (nextRandom) import Galley.App @@ -692,22 +690,18 @@ createConnectConversation loc a b name = do (lmems, rmems) <- addMembers lconv (UserList [a'] []) pure $ newConv conv ConnectConv a' lmems rmems [PrivateAccess] privateRole name Nothing Nothing Nothing -createOne2OneConversationRemote :: +createConnectConversationWithRemote :: MonadClient m => Local ConvId -> Local UserId -> - These (Local UserId) (Remote UserId) -> - m (Local ConvId) -createOne2OneConversationRemote _lconvId _creator _members = - error "Create conversation. If 'These' members a One2One conversation, otherwise a ConnectConversation" - -updateOne2OneConversationRemoteMembers :: - MonadClient m => - Local ConvId -> - These (Local UserId) (Remote UserId) -> - m (Local ConvId) -updateOne2OneConversationRemoteMembers = - error "Update members and conversation type." + UserList UserId -> + m () +createConnectConversationWithRemote lconvId creator m = do + retry x5 $ + write Cql.insertConv (params Quorum (lUnqualified lconvId, ConnectConv, lUnqualified creator, privateOnly, privateRole, Nothing, Nothing, Nothing, Nothing)) + -- We add only one member, second one gets added later, + -- when the other user accepts the connection request. + void $ addMembers lconvId m createLegacyOne2OneConversation :: MonadClient m => From 14f237201460c41c22a283a417fbae19bccf0e5a Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Wed, 6 Oct 2021 15:56:29 +0200 Subject: [PATCH 14/27] use schema-profunctor for json instances galley-types: no lax --- libs/galley-types/galley-types.cabal | 3 +- libs/galley-types/package.yaml | 1 + .../src/Galley/Types/Conversations/Remote.hs | 67 ++++++++++++------- 3 files changed, 44 insertions(+), 27 deletions(-) diff --git a/libs/galley-types/galley-types.cabal b/libs/galley-types/galley-types.cabal index 2b542d9c505..fc33a05e4d1 100644 --- a/libs/galley-types/galley-types.cabal +++ b/libs/galley-types/galley-types.cabal @@ -4,7 +4,7 @@ cabal-version: 1.12 -- -- see: https://github.com/sol/hpack -- --- hash: cc9f3ca760dba01b0959dd075d6c7b7d90a9ed8551322671bba7d7c9ca8fb64b +-- hash: 88ba6987ec832bc564d417a634a7ac00e226d35d09d84c5182bd53cbdd0b2c28 name: galley-types version: 0.81.0 @@ -43,6 +43,7 @@ library , exceptions >=0.10.0 , imports , lens >=4.12 + , schema-profunctor , string-conversions , tagged , text >=0.11 diff --git a/libs/galley-types/package.yaml b/libs/galley-types/package.yaml index 3c8971ad0a0..3d84f4036eb 100644 --- a/libs/galley-types/package.yaml +++ b/libs/galley-types/package.yaml @@ -21,6 +21,7 @@ library: - exceptions >=0.10.0 - lens >=4.12 - QuickCheck + - schema-profunctor - string-conversions - tagged - text >=0.11 diff --git a/libs/galley-types/src/Galley/Types/Conversations/Remote.hs b/libs/galley-types/src/Galley/Types/Conversations/Remote.hs index 5f06e32decb..78dc257b4a1 100644 --- a/libs/galley-types/src/Galley/Types/Conversations/Remote.hs +++ b/libs/galley-types/src/Galley/Types/Conversations/Remote.hs @@ -23,28 +23,37 @@ module Galley.Types.Conversations.Remote ) where -import Data.Aeson (FromJSON) -import Data.Aeson.Types (ToJSON) +import qualified Data.Aeson as A +import Data.Aeson.Types (FromJSON, ToJSON) import Data.Id (ConvId, UserId) -import Data.Qualified (Local, Qualified, Remote) +import Data.Qualified (Local, Qualified, Remote, toLocal, toRemote) +import Data.Schema +import Data.Tagged (Tagged (unTagged)) import Imports data DesiredMembership = Included | Excluded - deriving (Show, Generic) - --- TODO: write cusom instances with roundtrip tests -instance ToJSON DesiredMembership - --- TODO: write cusom instances with roundtrip tests -instance FromJSON DesiredMembership + deriving (Show, Eq, Generic) + deriving (FromJSON, ToJSON) via Schema DesiredMembership -data Actor = LocalActor | RemoteActor deriving (Show, Eq, Generic) +instance ToSchema DesiredMembership where + schema = + enum @Text "DesiredMembership" $ + mconcat + [ element "included" Included, + element "excluded" Excluded + ] --- TODO: write cusom instances with roundtrip tests -instance ToJSON Actor +data Actor = LocalActor | RemoteActor + deriving (Show, Eq, Generic) + deriving (FromJSON, ToJSON) via Schema Actor --- TODO: write cusom instances with roundtrip tests -instance FromJSON Actor +instance ToSchema Actor where + schema = + enum @Text "Actor" $ + mconcat + [ element "local_actor" LocalActor, + element "remote_actor" RemoteActor + ] data UpsertOne2OneConversationRequest = UpsertOne2OneConversationRequest { uooLocalUser :: Local UserId, @@ -54,23 +63,29 @@ data UpsertOne2OneConversationRequest = UpsertOne2OneConversationRequest uooConvId :: Maybe (Qualified ConvId) } deriving (Show, Generic) + deriving (FromJSON, ToJSON) via Schema UpsertOne2OneConversationRequest --- TODO: write cusom instances with roundtrip tests -instance ToJSON UpsertOne2OneConversationRequest +instance ToSchema UpsertOne2OneConversationRequest where + schema = + object "UpsertOne2OneConversationRequest" $ + UpsertOne2OneConversationRequest + <$> (unTagged . uooLocalUser) .= field "local_user" (toLocal <$> schema) + <*> (unTagged . uooRemoteUser) .= field "remote_user" (toRemote <$> schema) + <*> uooActor .= field "actor" schema + <*> uooActorDesiredMembership .= field "actor_desired_membership" schema + <*> uooConvId .= field "conversation_id" (optWithDefault A.Null schema) --- TODO: write cusom instances with roundtrip tests -instance FromJSON UpsertOne2OneConversationRequest - -data UpsertOne2OneConversationResponse = UpsertOne2OneConversationResponse +newtype UpsertOne2OneConversationResponse = UpsertOne2OneConversationResponse { -- | The Nothing value here indicated that there an impossible request was -- received, e.g., requesting to remove a remote user when the actor is a -- local user. uuorConvId :: Maybe (Qualified ConvId) } deriving (Show, Generic) + deriving (FromJSON, ToJSON) via Schema UpsertOne2OneConversationResponse --- TODO: write cusom instances with roundtrip tests -instance ToJSON UpsertOne2OneConversationResponse - --- TODO: write cusom instances with roundtrip tests -instance FromJSON UpsertOne2OneConversationResponse +instance ToSchema UpsertOne2OneConversationResponse where + schema = + object "UpsertOne2OneConversationResponse" $ + UpsertOne2OneConversationResponse + <$> uuorConvId .= field "conversation_id" (optWithDefault A.Null schema) From 03a16098d61174a97b337b4708d73c1cea905154 Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Wed, 6 Oct 2021 16:01:05 +0200 Subject: [PATCH 15/27] galley-types rename module to Intra --- libs/galley-types/galley-types.cabal | 4 ++-- .../src/Galley/Types/Conversations/{Remote.hs => Intra.hs} | 4 ++-- services/galley/src/Galley/API/Internal.hs | 2 +- services/galley/src/Galley/API/One2One.hs | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) rename libs/galley-types/src/Galley/Types/Conversations/{Remote.hs => Intra.hs} (97%) diff --git a/libs/galley-types/galley-types.cabal b/libs/galley-types/galley-types.cabal index fc33a05e4d1..8af1c5e5f5a 100644 --- a/libs/galley-types/galley-types.cabal +++ b/libs/galley-types/galley-types.cabal @@ -4,7 +4,7 @@ cabal-version: 1.12 -- -- see: https://github.com/sol/hpack -- --- hash: 88ba6987ec832bc564d417a634a7ac00e226d35d09d84c5182bd53cbdd0b2c28 +-- hash: d7419acbff460382bb822952b693f55513e729a4e3bcd0ddfdeea9e5285a805b name: galley-types version: 0.81.0 @@ -22,8 +22,8 @@ library Galley.Types Galley.Types.Bot Galley.Types.Bot.Service + Galley.Types.Conversations.Intra Galley.Types.Conversations.Members - Galley.Types.Conversations.Remote Galley.Types.Conversations.Roles Galley.Types.Teams Galley.Types.Teams.Intra diff --git a/libs/galley-types/src/Galley/Types/Conversations/Remote.hs b/libs/galley-types/src/Galley/Types/Conversations/Intra.hs similarity index 97% rename from libs/galley-types/src/Galley/Types/Conversations/Remote.hs rename to libs/galley-types/src/Galley/Types/Conversations/Intra.hs index 78dc257b4a1..e0b7dcb8804 100644 --- a/libs/galley-types/src/Galley/Types/Conversations/Remote.hs +++ b/libs/galley-types/src/Galley/Types/Conversations/Intra.hs @@ -14,8 +14,8 @@ -- -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . --- TODO: Rename to Galley.Types.Conversations.Intra -module Galley.Types.Conversations.Remote + +module Galley.Types.Conversations.Intra ( DesiredMembership (..), Actor (..), UpsertOne2OneConversationRequest (..), diff --git a/services/galley/src/Galley/API/Internal.hs b/services/galley/src/Galley/API/Internal.hs index 50721130624..925e5456f23 100644 --- a/services/galley/src/Galley/API/Internal.hs +++ b/services/galley/src/Galley/API/Internal.hs @@ -58,7 +58,7 @@ import qualified Galley.Queue as Q import Galley.Types import Galley.Types.Bot (AddBot, RemoveBot) import Galley.Types.Bot.Service -import Galley.Types.Conversations.Remote (UpsertOne2OneConversationRequest (..), UpsertOne2OneConversationResponse (..)) +import Galley.Types.Conversations.Intra (UpsertOne2OneConversationRequest (..), UpsertOne2OneConversationResponse (..)) import Galley.Types.Teams hiding (MemberLeave) import Galley.Types.Teams.Intra import Galley.Types.Teams.SearchVisibility diff --git a/services/galley/src/Galley/API/One2One.hs b/services/galley/src/Galley/API/One2One.hs index f29a2565883..bb7c0a8b848 100644 --- a/services/galley/src/Galley/API/One2One.hs +++ b/services/galley/src/Galley/API/One2One.hs @@ -37,7 +37,7 @@ import qualified Data.UUID as UUID import qualified Data.UUID.Tagged as U import Galley.App (Galley) import qualified Galley.Data as Data -import Galley.Types.Conversations.Remote (Actor (..), DesiredMembership (..), UpsertOne2OneConversationRequest (..), UpsertOne2OneConversationResponse (..)) +import Galley.Types.Conversations.Intra (Actor (..), DesiredMembership (..), UpsertOne2OneConversationRequest (..), UpsertOne2OneConversationResponse (..)) import Galley.Types.UserList (UserList (..)) import Imports From 6fcdc7bc7bc8320e2d88e8fd19f4906ea7becf6b Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Wed, 6 Oct 2021 16:04:15 +0200 Subject: [PATCH 16/27] galley: remove "these" dep galley.cabal --- services/galley/galley.cabal | 3 +-- services/galley/package.yaml | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal index 78022187a1f..9ed22c5b6c8 100644 --- a/services/galley/galley.cabal +++ b/services/galley/galley.cabal @@ -4,7 +4,7 @@ cabal-version: 1.12 -- -- see: https://github.com/sol/hpack -- --- hash: b493b5406526fa471b5bccba17b8a264edf4ce67115987c8666e729ec24c2a2c +-- hash: 10735fce03e5965b253e8d0360c4bfca3db877730b220367acd5d14083af69c5 name: galley version: 0.83.0 @@ -143,7 +143,6 @@ library , swagger2 , tagged , text >=0.11 - , these , time >=1.4 , tinylog >=0.10 , tls >=1.3.10 diff --git a/services/galley/package.yaml b/services/galley/package.yaml index cbf5fd04cb5..f993d78d0d8 100644 --- a/services/galley/package.yaml +++ b/services/galley/package.yaml @@ -82,7 +82,6 @@ library: - swagger >=0.1 - swagger2 - text >=0.11 - - these - time >=1.4 - tinylog >=0.10 - tls >=1.3.10 From 99ce2e38575aef635865e6a4f967878587236bf5 Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Wed, 6 Oct 2021 16:07:12 +0200 Subject: [PATCH 17/27] fix impossible example --- libs/galley-types/src/Galley/Types/Conversations/Intra.hs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/libs/galley-types/src/Galley/Types/Conversations/Intra.hs b/libs/galley-types/src/Galley/Types/Conversations/Intra.hs index e0b7dcb8804..5091ccd5329 100644 --- a/libs/galley-types/src/Galley/Types/Conversations/Intra.hs +++ b/libs/galley-types/src/Galley/Types/Conversations/Intra.hs @@ -77,8 +77,7 @@ instance ToSchema UpsertOne2OneConversationRequest where newtype UpsertOne2OneConversationResponse = UpsertOne2OneConversationResponse { -- | The Nothing value here indicated that there an impossible request was - -- received, e.g., requesting to remove a remote user when the actor is a - -- local user. + -- received, e.g., a remote actor for a remotely owned connect conversation uuorConvId :: Maybe (Qualified ConvId) } deriving (Show, Generic) From ba486ddce1c817ec691be31104fc4fd0118c5144 Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Wed, 6 Oct 2021 16:15:58 +0200 Subject: [PATCH 18/27] remove todo --- services/galley/src/Galley/API/Create.hs | 1 - 1 file changed, 1 deletion(-) diff --git a/services/galley/src/Galley/API/Create.hs b/services/galley/src/Galley/API/Create.hs index 919d5b4a46d..7a944cd9a5d 100644 --- a/services/galley/src/Galley/API/Create.hs +++ b/services/galley/src/Galley/API/Create.hs @@ -290,7 +290,6 @@ createConnectConversation usr conn j = do (createConnectConversationWithRemote lusr conn) (cRecipient j) --- TODO: remove createConnectConversationWithRemote :: Local UserId -> Maybe ConnId -> From c237ef4b7f20dcc5c66d580886cad894e00410b0 Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Wed, 6 Oct 2021 16:18:42 +0200 Subject: [PATCH 19/27] un-nameclash: one2OneConvId -> localOne2OneConvId --- .../src/Galley/Types/Conversations/Intra.hs | 7 +++--- services/galley/src/Galley/API/Create.hs | 4 +-- services/galley/src/Galley/API/One2One.hs | 25 +++++++++---------- services/galley/src/Galley/Data.hs | 12 ++++----- 4 files changed, 23 insertions(+), 25 deletions(-) diff --git a/libs/galley-types/src/Galley/Types/Conversations/Intra.hs b/libs/galley-types/src/Galley/Types/Conversations/Intra.hs index 5091ccd5329..f0e9828538d 100644 --- a/libs/galley-types/src/Galley/Types/Conversations/Intra.hs +++ b/libs/galley-types/src/Galley/Types/Conversations/Intra.hs @@ -26,9 +26,8 @@ where import qualified Data.Aeson as A import Data.Aeson.Types (FromJSON, ToJSON) import Data.Id (ConvId, UserId) -import Data.Qualified (Local, Qualified, Remote, toLocal, toRemote) +import Data.Qualified import Data.Schema -import Data.Tagged (Tagged (unTagged)) import Imports data DesiredMembership = Included | Excluded @@ -69,8 +68,8 @@ instance ToSchema UpsertOne2OneConversationRequest where schema = object "UpsertOne2OneConversationRequest" $ UpsertOne2OneConversationRequest - <$> (unTagged . uooLocalUser) .= field "local_user" (toLocal <$> schema) - <*> (unTagged . uooRemoteUser) .= field "remote_user" (toRemote <$> schema) + <$> (qUntagged . uooLocalUser) .= field "local_user" (qTagUnsafe <$> schema) + <*> (qUntagged . uooRemoteUser) .= field "remote_user" (qTagUnsafe <$> schema) <*> uooActor .= field "actor" schema <*> uooActorDesiredMembership .= field "actor_desired_membership" schema <*> uooConvId .= field "conversation_id" (optWithDefault A.Null schema) diff --git a/services/galley/src/Galley/API/Create.hs b/services/galley/src/Galley/API/Create.hs index 7a944cd9a5d..ccd977b2670 100644 --- a/services/galley/src/Galley/API/Create.hs +++ b/services/galley/src/Galley/API/Create.hs @@ -307,7 +307,7 @@ createLegacyConnectConversation :: createLegacyConnectConversation lusr conn lrecipient j = do (x, y) <- toUUIDs (tUnqualified lusr) (tUnqualified lrecipient) n <- rangeCheckedMaybe (cName j) - conv <- Data.conversation (Data.one2OneConvId x y) + conv <- Data.conversation (Data.localOne2OneConvId x y) maybe (create x y n) (update n) conv where create x y n = do @@ -408,7 +408,7 @@ notifyCreatedConversation dtime usr conn c = do localOne2OneConvId :: Local UserId -> Local UserId -> Galley (Local ConvId) localOne2OneConvId self other = do (x, y) <- toUUIDs (tUnqualified self) (tUnqualified other) - pure . qualifyAs self $ Data.one2OneConvId x y + pure . qualifyAs self $ Data.localOne2OneConvId x y toUUIDs :: UserId -> UserId -> Galley (U.UUID U.V4, U.UUID U.V4) toUUIDs a b = do diff --git a/services/galley/src/Galley/API/One2One.hs b/services/galley/src/Galley/API/One2One.hs index bb7c0a8b848..47efe9cf06f 100644 --- a/services/galley/src/Galley/API/One2One.hs +++ b/services/galley/src/Galley/API/One2One.hs @@ -31,7 +31,6 @@ import Data.ByteString.Conversion import qualified Data.ByteString.Lazy as L import Data.Id import Data.Qualified -import Data.Tagged (Tagged (unTagged)) import Data.UUID (UUID) import qualified Data.UUID as UUID import qualified Data.UUID.Tagged as U @@ -127,16 +126,16 @@ one2OneConvId a b = case compareDomains a b of iUpsertOne2OneConversation :: UpsertOne2OneConversationRequest -> Galley UpsertOne2OneConversationResponse iUpsertOne2OneConversation UpsertOne2OneConversationRequest {..} = do - let convId = fromMaybe (one2OneConvId (unTagged uooLocalUser) (unTagged uooRemoteUser)) uooConvId + let convId = fromMaybe (one2OneConvId (qUntagged uooLocalUser) (qUntagged uooRemoteUser)) uooConvId let dolocal :: Local ConvId -> Galley (Maybe (Qualified ConvId)) dolocal lconvId = do - mbConv <- Data.conversation (lUnqualified lconvId) + mbConv <- Data.conversation (tUnqualified lconvId) case mbConv of Nothing -> do let members = case (uooActor, uooActorDesiredMembership) of - (LocalActor, Included) -> UserList [lUnqualified uooLocalUser] [] + (LocalActor, Included) -> UserList [tUnqualified uooLocalUser] [] (LocalActor, Excluded) -> UserList [] [] (RemoteActor, Included) -> UserList [] [uooRemoteUser] (RemoteActor, Excluded) -> UserList [] [] @@ -150,23 +149,23 @@ iUpsertOne2OneConversation UpsertOne2OneConversationRequest {..} = do (LocalActor, Included) -> do void $ Data.addMember lconvId uooLocalUser unless (null (Data.convRemoteMembers conv)) $ - Data.acceptConnect (lUnqualified lconvId) - (LocalActor, Excluded) -> Data.removeMember (lUnqualified uooLocalUser) (lUnqualified lconvId) + Data.acceptConnect (tUnqualified lconvId) + (LocalActor, Excluded) -> Data.removeMember (tUnqualified uooLocalUser) (tUnqualified lconvId) (RemoteActor, Included) -> do void $ Data.addMembers lconvId (UserList [] [uooRemoteUser]) unless (null (Data.convLocalMembers conv)) $ - Data.acceptConnect (lUnqualified lconvId) - (RemoteActor, Excluded) -> Data.removeRemoteMembersFromLocalConv (lUnqualified lconvId) (pure uooRemoteUser) - pure . Just . unTagged $ lconvId + Data.acceptConnect (tUnqualified lconvId) + (RemoteActor, Excluded) -> Data.removeRemoteMembersFromLocalConv (tUnqualified lconvId) (pure uooRemoteUser) + pure . Just . qUntagged $ lconvId doremote :: Remote ConvId -> Galley (Maybe (Qualified ConvId)) doremote rconvId = case (uooActor, uooActorDesiredMembership) of (LocalActor, Included) -> do - Data.addLocalMembersToRemoteConv (unTagged rconvId) [lUnqualified uooLocalUser] - pure . Just . unTagged $ rconvId + Data.addLocalMembersToRemoteConv (qUntagged rconvId) [tUnqualified uooLocalUser] + pure . Just . qUntagged $ rconvId (LocalActor, Excluded) -> do - Data.removeLocalMembersFromRemoteConv (unTagged rconvId) [lUnqualified uooLocalUser] - pure . Just . unTagged $ rconvId + Data.removeLocalMembersFromRemoteConv (qUntagged rconvId) [tUnqualified uooLocalUser] + pure . Just . qUntagged $ rconvId (RemoteActor, _) -> pure Nothing UpsertOne2OneConversationResponse <$> foldQualified uooLocalUser dolocal doremote convId diff --git a/services/galley/src/Galley/Data.hs b/services/galley/src/Galley/Data.hs index 6160c09a17c..7cbbc2d77f2 100644 --- a/services/galley/src/Galley/Data.hs +++ b/services/galley/src/Galley/Data.hs @@ -114,7 +114,7 @@ module Galley.Data updateClient, -- * Utilities - one2OneConvId, + localOne2OneConvId, newMember, -- * Defaults @@ -680,7 +680,7 @@ createConnectConversation :: Maybe (Range 1 256 Text) -> m Conversation createConnectConversation loc a b name = do - let conv = one2OneConvId a b + let conv = localOne2OneConvId a b lconv = qualifyAs loc conv a' = Id . U.unpack $ a retry x5 $ @@ -698,7 +698,7 @@ createConnectConversationWithRemote :: m () createConnectConversationWithRemote lconvId creator m = do retry x5 $ - write Cql.insertConv (params Quorum (lUnqualified lconvId, ConnectConv, lUnqualified creator, privateOnly, privateRole, Nothing, Nothing, Nothing, Nothing)) + write Cql.insertConv (params Quorum (tUnqualified lconvId, ConnectConv, tUnqualified creator, privateOnly, privateRole, Nothing, Nothing, Nothing, Nothing)) -- We add only one member, second one gets added later, -- when the other user accepts the connection request. void $ addMembers lconvId m @@ -712,7 +712,7 @@ createLegacyOne2OneConversation :: Maybe TeamId -> m Conversation createLegacyOne2OneConversation loc a b name ti = do - let conv = one2OneConvId a b + let conv = localOne2OneConvId a b lconv = qualifyAs loc conv a' = Id (U.unpack a) b' = Id (U.unpack b) @@ -773,8 +773,8 @@ acceptConnect cid = retry x5 $ write Cql.updateConvType (params Quorum (One2OneC -- together pairwise, and then setting the version bits (v4) and variant bits -- (variant 2). This means that we always know what the UUID is for a -- one-to-one conversation which hopefully makes them unique. -one2OneConvId :: U.UUID U.V4 -> U.UUID U.V4 -> ConvId -one2OneConvId a b = Id . U.unpack $ U.addv4 a b +localOne2OneConvId :: U.UUID U.V4 -> U.UUID U.V4 -> ConvId +localOne2OneConvId a b = Id . U.unpack $ U.addv4 a b newConv :: ConvId -> From b7c464e57b4ad39823e3bbbe2bd061d2c3cba1fb Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Wed, 6 Oct 2021 16:20:05 +0200 Subject: [PATCH 20/27] remove warning suppression --- services/galley/test/unit/Test/Galley/API/One2One.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/galley/test/unit/Test/Galley/API/One2One.hs b/services/galley/test/unit/Test/Galley/API/One2One.hs index 13201e77037..d3f6f0332fe 100644 --- a/services/galley/test/unit/Test/Galley/API/One2One.hs +++ b/services/galley/test/unit/Test/Galley/API/One2One.hs @@ -1,4 +1,5 @@ {-# LANGUAGE NumericUnderscores #-} + -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2021 Wire Swiss GmbH @@ -15,7 +16,6 @@ -- -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -{-# OPTIONS_GHC -Wno-deferred-type-errors #-} -- | Tests for one-to-one conversations module Test.Galley.API.One2One where From cf435c0584805efef511eb9d77b409b2ac4d5d65 Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Wed, 6 Oct 2021 17:23:12 +0200 Subject: [PATCH 21/27] brig: add rpc function --- services/brig/src/Brig/IO/Intra.hs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/services/brig/src/Brig/IO/Intra.hs b/services/brig/src/Brig/IO/Intra.hs index 301beea269a..271c72b7f80 100644 --- a/services/brig/src/Brig/IO/Intra.hs +++ b/services/brig/src/Brig/IO/Intra.hs @@ -32,6 +32,7 @@ module Brig.IO.Intra blockConv, unblockConv, getConv, + upsertOne2OneConversation, -- * Clients Brig.IO.Intra.newClient, @@ -93,6 +94,7 @@ import Data.Qualified import Data.Range import qualified Data.Set as Set import Galley.Types (Connect (..), Conversation) +import Galley.Types.Conversations.Intra (UpsertOne2OneConversationRequest, UpsertOne2OneConversationResponse) import qualified Galley.Types.Teams as Team import Galley.Types.Teams.Intra (GuardLegalholdPolicyConflicts (GuardLegalholdPolicyConflicts)) import qualified Galley.Types.Teams.Intra as Team @@ -657,6 +659,18 @@ getConv usr cnv = do . zUser usr . expect [status200, status404] +upsertOne2OneConversation :: UpsertOne2OneConversationRequest -> AppIO UpsertOne2OneConversationResponse +upsertOne2OneConversation urequest = do + response <- galleyRequest POST req + case Bilge.statusCode response of + 200 -> decodeBody "galley" response + _ -> throwM internalServerError + where + req = + paths ["i", "conversations", "one2one", "upsert"] + . header "Content-Type" "application/json" + . lbytes (encode urequest) + -- | Calls 'Galley.API.getTeamConversationH'. getTeamConv :: UserId -> TeamId -> ConvId -> AppIO (Maybe Team.TeamConversation) getTeamConv usr tid cnv = do From 62836c397d6cd2fcc2bcb3bb36fa3d90dadafb8e Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Thu, 7 Oct 2021 16:19:36 +0200 Subject: [PATCH 22/27] change api: alwyas return a conv id --- .../src/Galley/Types/Conversations/Intra.hs | 4 ++-- services/galley/src/Galley/API/One2One.hs | 19 +++++++------------ 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/libs/galley-types/src/Galley/Types/Conversations/Intra.hs b/libs/galley-types/src/Galley/Types/Conversations/Intra.hs index f0e9828538d..4644a1bc112 100644 --- a/libs/galley-types/src/Galley/Types/Conversations/Intra.hs +++ b/libs/galley-types/src/Galley/Types/Conversations/Intra.hs @@ -77,7 +77,7 @@ instance ToSchema UpsertOne2OneConversationRequest where newtype UpsertOne2OneConversationResponse = UpsertOne2OneConversationResponse { -- | The Nothing value here indicated that there an impossible request was -- received, e.g., a remote actor for a remotely owned connect conversation - uuorConvId :: Maybe (Qualified ConvId) + uuorConvId :: Qualified ConvId } deriving (Show, Generic) deriving (FromJSON, ToJSON) via Schema UpsertOne2OneConversationResponse @@ -86,4 +86,4 @@ instance ToSchema UpsertOne2OneConversationResponse where schema = object "UpsertOne2OneConversationResponse" $ UpsertOne2OneConversationResponse - <$> uuorConvId .= field "conversation_id" (optWithDefault A.Null schema) + <$> uuorConvId .= field "conversation_id" schema diff --git a/services/galley/src/Galley/API/One2One.hs b/services/galley/src/Galley/API/One2One.hs index 47efe9cf06f..fa9de3f254b 100644 --- a/services/galley/src/Galley/API/One2One.hs +++ b/services/galley/src/Galley/API/One2One.hs @@ -128,7 +128,7 @@ iUpsertOne2OneConversation :: UpsertOne2OneConversationRequest -> Galley UpsertO iUpsertOne2OneConversation UpsertOne2OneConversationRequest {..} = do let convId = fromMaybe (one2OneConvId (qUntagged uooLocalUser) (qUntagged uooRemoteUser)) uooConvId - let dolocal :: Local ConvId -> Galley (Maybe (Qualified ConvId)) + let dolocal :: Local ConvId -> Galley () dolocal lconvId = do mbConv <- Data.conversation (tUnqualified lconvId) case mbConv of @@ -139,11 +139,8 @@ iUpsertOne2OneConversation UpsertOne2OneConversationRequest {..} = do (LocalActor, Excluded) -> UserList [] [] (RemoteActor, Included) -> UserList [] [uooRemoteUser] (RemoteActor, Excluded) -> UserList [] [] - case members of - UserList [] [] -> pure Nothing - _ -> do - Data.createConnectConversationWithRemote lconvId uooLocalUser members - pure . Just $ convId + unless (null members) $ + Data.createConnectConversationWithRemote lconvId uooLocalUser members Just conv -> do case (uooActor, uooActorDesiredMembership) of (LocalActor, Included) -> do @@ -156,16 +153,14 @@ iUpsertOne2OneConversation UpsertOne2OneConversationRequest {..} = do unless (null (Data.convLocalMembers conv)) $ Data.acceptConnect (tUnqualified lconvId) (RemoteActor, Excluded) -> Data.removeRemoteMembersFromLocalConv (tUnqualified lconvId) (pure uooRemoteUser) - pure . Just . qUntagged $ lconvId - doremote :: Remote ConvId -> Galley (Maybe (Qualified ConvId)) + doremote :: Remote ConvId -> Galley () doremote rconvId = case (uooActor, uooActorDesiredMembership) of (LocalActor, Included) -> do Data.addLocalMembersToRemoteConv (qUntagged rconvId) [tUnqualified uooLocalUser] - pure . Just . qUntagged $ rconvId (LocalActor, Excluded) -> do Data.removeLocalMembersFromRemoteConv (qUntagged rconvId) [tUnqualified uooLocalUser] - pure . Just . qUntagged $ rconvId - (RemoteActor, _) -> pure Nothing + (RemoteActor, _) -> pure () - UpsertOne2OneConversationResponse <$> foldQualified uooLocalUser dolocal doremote convId + foldQualified uooLocalUser dolocal doremote convId + pure (UpsertOne2OneConversationResponse convId) From 60a6f793b5dfe2d95e0255dc8943d19e0d2e5949 Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Thu, 7 Oct 2021 18:00:59 +0200 Subject: [PATCH 23/27] Add tests for one2one conversation internal endpoint --- services/galley/galley.cabal | 3 +- services/galley/package.yaml | 1 + services/galley/src/Galley/Data.hs | 4 +- services/galley/test/integration/API.hs | 61 ++++++++++++++++++-- services/galley/test/integration/API/Util.hs | 6 ++ 5 files changed, 68 insertions(+), 7 deletions(-) diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal index 9ed22c5b6c8..57fabd19156 100644 --- a/services/galley/galley.cabal +++ b/services/galley/galley.cabal @@ -4,7 +4,7 @@ cabal-version: 1.12 -- -- see: https://github.com/sol/hpack -- --- hash: 10735fce03e5965b253e8d0360c4bfca3db877730b220367acd5d14083af69c5 +-- hash: c121411458d6b0f7118ae1589134cb37711d29cad840e07d0f135663c59cc53a name: galley version: 0.83.0 @@ -235,6 +235,7 @@ executable galley-integration , cereal , containers , cookie + , cql-io , currency-codes , data-timeout , errors diff --git a/services/galley/package.yaml b/services/galley/package.yaml index f993d78d0d8..2ae2bfe98a5 100644 --- a/services/galley/package.yaml +++ b/services/galley/package.yaml @@ -165,6 +165,7 @@ executables: - cereal - containers - cookie + - cql-io - currency-codes - metrics-wai - data-timeout diff --git a/services/galley/src/Galley/Data.hs b/services/galley/src/Galley/Data.hs index 7cbbc2d77f2..0e3267a7f54 100644 --- a/services/galley/src/Galley/Data.hs +++ b/services/galley/src/Galley/Data.hs @@ -882,7 +882,7 @@ toRemoteMember :: UserId -> Domain -> RoleName -> RemoteMember toRemoteMember u d = RemoteMember (toRemoteUnsafe d u) memberLists :: - (MonadClient m, Log.MonadLogger m, MonadThrow m) => + (MonadClient m, MonadThrow m) => [ConvId] -> m [[LocalMember]] memberLists convs = do @@ -897,7 +897,7 @@ memberLists convs = do mkMem (cnv, usr, srv, prv, st, omus, omur, oar, oarr, hid, hidr, crn) = (cnv, toMember (usr, srv, prv, st, omus, omur, oar, oarr, hid, hidr, crn)) -members :: (MonadClient m, Log.MonadLogger m, MonadThrow m) => ConvId -> m [LocalMember] +members :: (MonadClient m, MonadThrow m) => ConvId -> m [LocalMember] members conv = join <$> memberLists [conv] lookupRemoteMembers :: (MonadClient m) => ConvId -> m [RemoteMember] diff --git a/services/galley/test/integration/API.hs b/services/galley/test/integration/API.hs index 7d0b95730bf..7a3589e592a 100644 --- a/services/galley/test/integration/API.hs +++ b/services/galley/test/integration/API.hs @@ -56,12 +56,17 @@ import Data.Qualified import Data.Range import qualified Data.Set as Set import Data.String.Conversions (cs) +import Data.Tagged (unTagged) import qualified Data.Text as T import qualified Data.Text.Ascii as Ascii import Data.Time.Clock (getCurrentTime) +import Database.CQL.IO import Galley.API.Mapping +import Galley.API.One2One (one2OneConvId) +import qualified Galley.Data as Data import Galley.Options (Opts, optFederator) import Galley.Types hiding (LocalMember (..)) +import Galley.Types.Conversations.Intra import Galley.Types.Conversations.Members import Galley.Types.Conversations.Roles import qualified Galley.Types.Teams as Teams @@ -215,7 +220,8 @@ tests s = test s "convert invite to code-access conversation" postConvertCodeConv, test s "convert code to team-access conversation" postConvertTeamConv, test s "cannot join private conversation" postJoinConvFail, - test s "remove user" removeUser + test s "remove user" removeUser, + test s "iUpsertOne2OneConversation" testAllOne2OneConversationRequests ] emptyFederatedBrig :: FederatedBrig.Api (AsServerT Handler) @@ -1573,11 +1579,11 @@ postConnectConvOk2 :: TestM () postConnectConvOk2 = do alice <- randomUser bob <- randomUser - m <- decodeConvId <$> request alice bob - n <- decodeConvId <$> request alice bob + m <- decodeConvId <$> req alice bob + n <- decodeConvId <$> req alice bob liftIO $ m @=? n where - request alice bob = + req alice bob = postConnectConv alice bob "Alice" "connect with me!" (Just "me@me.com") putConvAcceptOk :: TestM () @@ -2942,3 +2948,50 @@ removeUser = do omService = Nothing, omConvRoleName = roleNameWireAdmin } + +testAllOne2OneConversationRequests :: TestM () +testAllOne2OneConversationRequests = do + for_ [LocalActor, RemoteActor] $ \actor -> + for_ [Included, Excluded] $ \desired -> + testOne2OneConversationRequest True actor desired + +testOne2OneConversationRequest :: Bool -> Actor -> DesiredMembership -> TestM () +testOne2OneConversationRequest shouldBeLocal actor desired = do + alice <- toLocal <$> randomQualifiedUser + (bob, expectedConvId) <- generateRemoteAndConvId alice + + convId <- do + let req = UpsertOne2OneConversationRequest alice bob actor desired Nothing + res <- + iUpsertOne2OneConversation req + responseJsonError res + + case shouldBeLocal of + True -> do + liftIO $ convId @?= expectedConvId + + mems <- getConvMembers alice (qUnqualified convId) + let actorId = case actor of + LocalActor -> unTagged alice + RemoteActor -> unTagged bob + liftIO $ isJust (find (actorId ==) mems) @?= (desired == Included) + liftIO $ filter (actorId /=) mems @?= [] + False -> pure () + where + getConvMembers :: Local UserId -> ConvId -> TestM [Qualified UserId] + getConvMembers lusr convId = do + db <- view tsCass + runClient db $ do + lmems <- fmap (unTagged . qualifyAs lusr . lmId) <$> Data.members convId + rmems <- fmap (unTagged . rmId) <$> Data.lookupRemoteMembers convId + pure (lmems <> rmems) + + generateRemoteAndConvId :: Local UserId -> TestM (Remote UserId, Qualified ConvId) + generateRemoteAndConvId lUserId = do + other <- Qualified <$> randomId <*> pure (Domain "far-away.example.com") + let convId = one2OneConvId (unTagged lUserId) other + isLocal = lDomain lUserId == qDomain convId + if shouldBeLocal == isLocal + then pure (toRemote other, convId) + else generateRemoteAndConvId lUserId diff --git a/services/galley/test/integration/API/Util.hs b/services/galley/test/integration/API/Util.hs index 140011ee079..aa6549d3ebf 100644 --- a/services/galley/test/integration/API/Util.hs +++ b/services/galley/test/integration/API/Util.hs @@ -71,6 +71,7 @@ import qualified Galley.Options as Opts import qualified Galley.Run as Run import Galley.Types import qualified Galley.Types as Conv +import Galley.Types.Conversations.Intra (UpsertOne2OneConversationRequest (..)) import Galley.Types.Conversations.Roles hiding (DeleteConversation) import Galley.Types.Teams hiding (Event, EventType (..)) import qualified Galley.Types.Teams as Team @@ -2329,3 +2330,8 @@ fedRequestsForDomain domain component = assertOne :: (HasCallStack, MonadIO m, Show a) => [a] -> m a assertOne [a] = pure a assertOne xs = liftIO . assertFailure $ "Expected exactly one element, found " <> show xs + +iUpsertOne2OneConversation :: UpsertOne2OneConversationRequest -> TestM ResponseLBS +iUpsertOne2OneConversation req = do + galley <- view tsGalley + post (galley . path "/i/conversations/one2one/upsert" . Bilge.json req) From 3555df950e28da178aa13520eb7db2b9040e4d06 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Thu, 7 Oct 2021 19:42:14 +0200 Subject: [PATCH 24/27] Test remote one2one conversation case --- services/galley/test/integration/API.hs | 43 +++++++++++++------------ 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/services/galley/test/integration/API.hs b/services/galley/test/integration/API.hs index 7a3589e592a..5a3b6e8cb3d 100644 --- a/services/galley/test/integration/API.hs +++ b/services/galley/test/integration/API.hs @@ -56,7 +56,6 @@ import Data.Qualified import Data.Range import qualified Data.Set as Set import Data.String.Conversions (cs) -import Data.Tagged (unTagged) import qualified Data.Text as T import qualified Data.Text.Ascii as Ascii import Data.Time.Clock (getCurrentTime) @@ -2953,12 +2952,14 @@ testAllOne2OneConversationRequests :: TestM () testAllOne2OneConversationRequests = do for_ [LocalActor, RemoteActor] $ \actor -> for_ [Included, Excluded] $ \desired -> - testOne2OneConversationRequest True actor desired + for_ [True, False] $ \shouldBeLocal -> + testOne2OneConversationRequest shouldBeLocal actor desired testOne2OneConversationRequest :: Bool -> Actor -> DesiredMembership -> TestM () testOne2OneConversationRequest shouldBeLocal actor desired = do - alice <- toLocal <$> randomQualifiedUser + alice <- qTagUnsafe <$> randomQualifiedUser (bob, expectedConvId) <- generateRemoteAndConvId alice + db <- view tsCass convId <- do let req = UpsertOne2OneConversationRequest alice bob actor desired Nothing @@ -2967,31 +2968,33 @@ testOne2OneConversationRequest shouldBeLocal actor desired = do responseJsonError res + liftIO $ convId @?= expectedConvId + case shouldBeLocal of True -> do - liftIO $ convId @?= expectedConvId - - mems <- getConvMembers alice (qUnqualified convId) + mems <- runClient db $ do + lmems <- fmap (qUntagged . qualifyAs alice . lmId) <$> Data.members (qUnqualified convId) + rmems <- fmap (qUntagged . rmId) <$> Data.lookupRemoteMembers (qUnqualified convId) + pure (lmems <> rmems) let actorId = case actor of - LocalActor -> unTagged alice - RemoteActor -> unTagged bob + LocalActor -> qUntagged alice + RemoteActor -> qUntagged bob liftIO $ isJust (find (actorId ==) mems) @?= (desired == Included) liftIO $ filter (actorId /=) mems @?= [] - False -> pure () + False -> do + mems <- runClient db $ do + smap <- Data.remoteConversationStatus (tUnqualified alice) [qTagUnsafe convId] + case Map.lookup (qTagUnsafe convId) smap of + Just _ -> pure [qUntagged alice] + _ -> pure [] + when (actor == LocalActor) $ + liftIO $ isJust (find (qUntagged alice ==) mems) @?= (desired == Included) where - getConvMembers :: Local UserId -> ConvId -> TestM [Qualified UserId] - getConvMembers lusr convId = do - db <- view tsCass - runClient db $ do - lmems <- fmap (unTagged . qualifyAs lusr . lmId) <$> Data.members convId - rmems <- fmap (unTagged . rmId) <$> Data.lookupRemoteMembers convId - pure (lmems <> rmems) - generateRemoteAndConvId :: Local UserId -> TestM (Remote UserId, Qualified ConvId) generateRemoteAndConvId lUserId = do other <- Qualified <$> randomId <*> pure (Domain "far-away.example.com") - let convId = one2OneConvId (unTagged lUserId) other - isLocal = lDomain lUserId == qDomain convId + let convId = one2OneConvId (qUntagged lUserId) other + isLocal = tDomain lUserId == qDomain convId if shouldBeLocal == isLocal - then pure (toRemote other, convId) + then pure (qTagUnsafe other, convId) else generateRemoteAndConvId lUserId From e4d2a14b4a4ae82bc0968a1a6101eaa9c89c5625 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Fri, 8 Oct 2021 09:34:48 +0200 Subject: [PATCH 25/27] Update golden tests after change in connect event --- libs/wire-api/test/golden/testObject_Connect_user_1.json | 4 ++++ libs/wire-api/test/golden/testObject_Connect_user_2.json | 4 ++++ libs/wire-api/test/golden/testObject_Event_user_10.json | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/libs/wire-api/test/golden/testObject_Connect_user_1.json b/libs/wire-api/test/golden/testObject_Connect_user_1.json index 551cb7d2b1e..63d654e6666 100644 --- a/libs/wire-api/test/golden/testObject_Connect_user_1.json +++ b/libs/wire-api/test/golden/testObject_Connect_user_1.json @@ -2,5 +2,9 @@ "email": "test email", "message": "E", "name": ".🝊]G", + "qualified_recipient": { + "domain": "foo.example.com", + "id": "00000002-0000-0001-0000-000400000004" + }, "recipient": "00000002-0000-0001-0000-000400000004" } diff --git a/libs/wire-api/test/golden/testObject_Connect_user_2.json b/libs/wire-api/test/golden/testObject_Connect_user_2.json index 8ce9a35dd43..76257ece7c0 100644 --- a/libs/wire-api/test/golden/testObject_Connect_user_2.json +++ b/libs/wire-api/test/golden/testObject_Connect_user_2.json @@ -2,5 +2,9 @@ "email": null, "message": null, "name": null, + "qualified_recipient": { + "domain": "bar.example.com", + "id": "00000005-0000-0007-0000-000200000008" + }, "recipient": "00000005-0000-0007-0000-000200000008" } diff --git a/libs/wire-api/test/golden/testObject_Event_user_10.json b/libs/wire-api/test/golden/testObject_Event_user_10.json index 3ec9ea0854c..7a1f9dcd990 100644 --- a/libs/wire-api/test/golden/testObject_Event_user_10.json +++ b/libs/wire-api/test/golden/testObject_Event_user_10.json @@ -4,6 +4,10 @@ "email": "󲛚", "message": "L", "name": "fq", + "qualified_recipient": { + "domain": "faraway.example.com", + "id": "00000008-0000-0000-0000-000600000001" + }, "recipient": "00000008-0000-0000-0000-000600000001" }, "from": "00007f28-0000-40b1-0000-56ab0000748d", From c27cc2d45f478b1c6be6c15f40b1fe0a024b1782 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Fri, 8 Oct 2021 09:39:54 +0200 Subject: [PATCH 26/27] Add CHANGELOG entry --- changelog.d/5-internal/one2one-upsert | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/5-internal/one2one-upsert diff --git a/changelog.d/5-internal/one2one-upsert b/changelog.d/5-internal/one2one-upsert new file mode 100644 index 00000000000..5371eb9a786 --- /dev/null +++ b/changelog.d/5-internal/one2one-upsert @@ -0,0 +1 @@ +Add internal endpoint to insert or update a 1-1 conversation. This is to be used by brig when updating the status of a connection. From b56d4fe24f668c27b9f626b3cde28e7b7a43478a Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Fri, 8 Oct 2021 10:11:44 +0200 Subject: [PATCH 27/27] Remove incorrect comment --- libs/galley-types/src/Galley/Types/Conversations/Intra.hs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/libs/galley-types/src/Galley/Types/Conversations/Intra.hs b/libs/galley-types/src/Galley/Types/Conversations/Intra.hs index 4644a1bc112..0cb0ba9afd7 100644 --- a/libs/galley-types/src/Galley/Types/Conversations/Intra.hs +++ b/libs/galley-types/src/Galley/Types/Conversations/Intra.hs @@ -75,9 +75,7 @@ instance ToSchema UpsertOne2OneConversationRequest where <*> uooConvId .= field "conversation_id" (optWithDefault A.Null schema) newtype UpsertOne2OneConversationResponse = UpsertOne2OneConversationResponse - { -- | The Nothing value here indicated that there an impossible request was - -- received, e.g., a remote actor for a remotely owned connect conversation - uuorConvId :: Qualified ConvId + { uuorConvId :: Qualified ConvId } deriving (Show, Generic) deriving (FromJSON, ToJSON) via Schema UpsertOne2OneConversationResponse