diff --git a/changelog.d/6-federation/self-member-status b/changelog.d/6-federation/self-member-status
new file mode 100644
index 0000000000..92794080bc
--- /dev/null
+++ b/changelog.d/6-federation/self-member-status
@@ -0,0 +1 @@
+Added support for updating self member status of remote conversations
diff --git a/docs/reference/cassandra-schema.cql b/docs/reference/cassandra-schema.cql
index e9383b3618..fd39a8753e 100644
--- a/docs/reference/cassandra-schema.cql
+++ b/docs/reference/cassandra-schema.cql
@@ -105,6 +105,12 @@ CREATE TABLE galley_test.user_remote_conv (
user uuid,
conv_remote_domain text,
conv_remote_id uuid,
+ hidden boolean,
+ hidden_ref text,
+ otr_archived boolean,
+ otr_archived_ref text,
+ otr_muted_ref text,
+ otr_muted_status int,
PRIMARY KEY (user, conv_remote_domain, conv_remote_id)
) WITH CLUSTERING ORDER BY (conv_remote_domain ASC, conv_remote_id ASC)
AND bloom_filter_fp_chance = 0.1
diff --git a/libs/galley-types/galley-types.cabal b/libs/galley-types/galley-types.cabal
index 30ec27537f..891555cac7 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: ebbe442ba952db0f975a3f93ffa72db3b1b971f506a30baaca5615f93b4b376a
+-- hash: 8d07ea070b6384ec247f4473abb198bbb9639f72543920cbe46f561df96963ca
name: galley-types
version: 0.81.0
@@ -43,6 +43,7 @@ library
, imports
, lens >=4.12
, string-conversions
+ , tagged
, text >=0.11
, time >=1.4
, types-common >=0.16
diff --git a/libs/galley-types/package.yaml b/libs/galley-types/package.yaml
index 56f849f9c9..3c8971ad0a 100644
--- a/libs/galley-types/package.yaml
+++ b/libs/galley-types/package.yaml
@@ -22,6 +22,7 @@ library:
- lens >=4.12
- QuickCheck
- string-conversions
+ - tagged
- text >=0.11
- time >=1.4
- types-common >=0.16
diff --git a/libs/galley-types/src/Galley/Types.hs b/libs/galley-types/src/Galley/Types.hs
index 09edac1c76..9a9e2cdca8 100644
--- a/libs/galley-types/src/Galley/Types.hs
+++ b/libs/galley-types/src/Galley/Types.hs
@@ -20,13 +20,21 @@
module Galley.Types
( foldrOtrRecipients,
Accept (..),
- ConversationMeta (..),
-- * re-exports
+ ConversationMetadata (..),
Conversation (..),
- LocalMember,
- RemoteMember,
- InternalMember (..),
+ cnvQualifiedId,
+ cnvType,
+ cnvCreator,
+ cnvAccess,
+ cnvAccessRole,
+ cnvName,
+ cnvTeam,
+ cnvMessageTimer,
+ cnvReceiptMode,
+ RemoteMember (..),
+ LocalMember (..),
ConvMembers (..),
OtherMember (..),
Connect (..),
@@ -74,11 +82,9 @@ module Galley.Types
where
import Data.Aeson
-import Data.Id (ClientId, ConvId, TeamId, UserId)
-import Data.Json.Util ((#))
+import Data.Id (ClientId, UserId)
import qualified Data.Map.Strict as Map
-import Data.Misc (Milliseconds)
-import Galley.Types.Conversations.Members (InternalMember (..), LocalMember, RemoteMember)
+import Galley.Types.Conversations.Members (LocalMember (..), RemoteMember (..))
import Imports
import Wire.API.Conversation hiding (Member (..))
import Wire.API.Conversation.Code
@@ -89,48 +95,6 @@ import Wire.API.Message
import Wire.API.User (UserIdList (..))
import Wire.API.User.Client
---------------------------------------------------------------------------------
--- ConversationMeta
-
-data ConversationMeta = ConversationMeta
- { cmId :: !ConvId,
- cmType :: !ConvType,
- cmCreator :: !UserId,
- cmAccess :: ![Access],
- cmAccessRole :: !AccessRole,
- cmName :: !(Maybe Text),
- cmTeam :: !(Maybe TeamId),
- cmMessageTimer :: !(Maybe Milliseconds),
- cmReceiptMode :: !(Maybe ReceiptMode)
- }
- deriving (Eq, Show)
-
-instance ToJSON ConversationMeta where
- toJSON c =
- object $
- "id" .= cmId c
- # "type" .= cmType c
- # "creator" .= cmCreator c
- # "access" .= cmAccess c
- # "access_role" .= cmAccessRole c
- # "name" .= cmName c
- # "team" .= cmTeam c
- # "message_timer" .= cmMessageTimer c
- # "receipt_mode" .= cmReceiptMode c
- # []
-
-instance FromJSON ConversationMeta where
- parseJSON = withObject "conversation-meta" $ \o ->
- ConversationMeta <$> o .: "id"
- <*> o .: "type"
- <*> o .: "creator"
- <*> o .: "access"
- <*> o .: "access_role"
- <*> o .: "name"
- <*> o .:? "team"
- <*> o .:? "message_timer"
- <*> o .:? "receipt_mode"
-
--------------------------------------------------------------------------------
-- Accept
diff --git a/libs/galley-types/src/Galley/Types/Conversations/Members.hs b/libs/galley-types/src/Galley/Types/Conversations/Members.hs
index c88e46c76e..7e6a88c6db 100644
--- a/libs/galley-types/src/Galley/Types/Conversations/Members.hs
+++ b/libs/galley-types/src/Galley/Types/Conversations/Members.hs
@@ -18,37 +18,73 @@
-- with this program. If not, see .
module Galley.Types.Conversations.Members
- ( LocalMember,
- RemoteMember (..),
- InternalMember (..),
+ ( RemoteMember (..),
+ remoteMemberToOther,
+ LocalMember (..),
+ localMemberToOther,
+ MemberStatus (..),
+ defMemberStatus,
)
where
+import Data.Domain
import Data.Id as Id
-import Data.Qualified (Remote)
+import Data.Qualified
+import Data.Tagged
import Imports
-import Wire.API.Conversation.Member (MutedStatus)
+import Wire.API.Conversation
import Wire.API.Conversation.Role (RoleName)
import Wire.API.Provider.Service (ServiceRef)
-type LocalMember = InternalMember Id.UserId
-
+-- | Internal (cassandra) representation of a remote conversation member.
data RemoteMember = RemoteMember
{ rmId :: Remote UserId,
rmConvRoleName :: RoleName
}
deriving stock (Show)
--- | Internal (cassandra) representation of a conversation member.
-data InternalMember id = InternalMember
- { memId :: id,
- memService :: Maybe ServiceRef,
- memOtrMutedStatus :: Maybe MutedStatus,
- memOtrMutedRef :: Maybe Text,
- memOtrArchived :: Bool,
- memOtrArchivedRef :: Maybe Text,
- memHidden :: Bool,
- memHiddenRef :: Maybe Text,
- memConvRoleName :: RoleName
+remoteMemberToOther :: RemoteMember -> OtherMember
+remoteMemberToOther x =
+ OtherMember
+ { omQualifiedId = unTagged (rmId x),
+ omService = Nothing,
+ omConvRoleName = rmConvRoleName x
+ }
+
+-- | Internal (cassandra) representation of a local conversation member.
+data LocalMember = LocalMember
+ { lmId :: UserId,
+ lmStatus :: MemberStatus,
+ lmService :: Maybe ServiceRef,
+ lmConvRoleName :: RoleName
+ }
+ deriving stock (Show)
+
+localMemberToOther :: Domain -> LocalMember -> OtherMember
+localMemberToOther domain x =
+ OtherMember
+ { omQualifiedId = Qualified (lmId x) domain,
+ omService = lmService x,
+ omConvRoleName = lmConvRoleName x
+ }
+
+data MemberStatus = MemberStatus
+ { msOtrMutedStatus :: Maybe MutedStatus,
+ msOtrMutedRef :: Maybe Text,
+ msOtrArchived :: Bool,
+ msOtrArchivedRef :: Maybe Text,
+ msHidden :: Bool,
+ msHiddenRef :: Maybe Text
}
- deriving stock (Functor, Show)
+ deriving stock (Show)
+
+defMemberStatus :: MemberStatus
+defMemberStatus =
+ MemberStatus
+ { msOtrMutedStatus = Nothing,
+ msOtrMutedRef = Nothing,
+ msOtrArchived = False,
+ msOtrArchivedRef = Nothing,
+ msHidden = False,
+ msHiddenRef = Nothing
+ }
diff --git a/libs/types-common/src/Data/Qualified.hs b/libs/types-common/src/Data/Qualified.hs
index d116210003..779cae9908 100644
--- a/libs/types-common/src/Data/Qualified.hs
+++ b/libs/types-common/src/Data/Qualified.hs
@@ -23,6 +23,8 @@ module Data.Qualified
Qualified (..),
Remote,
toRemote,
+ Local,
+ toLocal,
renderQualifiedId,
partitionRemoteOrLocalIds,
partitionRemoteOrLocalIds',
@@ -66,6 +68,13 @@ type Remote a = Tagged "remote" (Qualified a)
toRemote :: Qualified a -> Remote a
toRemote = Tagged
+-- | A type representing a Qualified value where the domain is guaranteed to be
+-- the local one.
+type Local a = Tagged "local" (Qualified a)
+
+toLocal :: Qualified a -> Local a
+toLocal = Tagged
+
-- | FUTUREWORK: Maybe delete this, it is only used in printing federation not
-- implemented errors
renderQualified :: (a -> Text) -> Qualified a -> Text
diff --git a/libs/wire-api-federation/src/Wire/API/Federation/API/Galley.hs b/libs/wire-api-federation/src/Wire/API/Federation/API/Galley.hs
index 02a46ed39e..d80a55a5e2 100644
--- a/libs/wire-api-federation/src/Wire/API/Federation/API/Galley.hs
+++ b/libs/wire-api-federation/src/Wire/API/Federation/API/Galley.hs
@@ -30,8 +30,7 @@ import Servant.API (JSON, Post, ReqBody, Summary, (:>))
import Servant.API.Generic ((:-))
import Servant.Client.Generic (AsClientT, genericClient)
import Wire.API.Arbitrary (Arbitrary, GenericUniform (..))
-import Wire.API.Conversation (Access, AccessRole, ConvType, Conversation, ReceiptMode)
-import Wire.API.Conversation.Member (OtherMember)
+import Wire.API.Conversation
import Wire.API.Conversation.Role (RoleName)
import Wire.API.Federation.Client (FederationClientFailure, FederatorClient)
import Wire.API.Federation.Domain (OriginDomainHeader)
@@ -60,6 +59,7 @@ data Api routes = Api
routes
:- "federation"
:> "get-conversations"
+ :> OriginDomainHeader
:> ReqBody '[JSON] GetConversationsRequest
:> Post '[JSON] GetConversationsResponse,
-- used by backend that owns the conversation to inform the backend about
@@ -100,15 +100,35 @@ data Api routes = Api
deriving (Generic)
data GetConversationsRequest = GetConversationsRequest
- { gcrUserId :: Qualified UserId,
+ { gcrUserId :: UserId,
gcrConvIds :: [ConvId]
}
deriving stock (Eq, Show, Generic)
deriving (Arbitrary) via (GenericUniform GetConversationsRequest)
deriving (ToJSON, FromJSON) via (CustomEncoded GetConversationsRequest)
+data RemoteConvMembers = RemoteConvMembers
+ { rcmSelfRole :: RoleName,
+ rcmOthers :: [OtherMember]
+ }
+ deriving stock (Eq, Show, Generic)
+ deriving (Arbitrary) via (GenericUniform RemoteConvMembers)
+ deriving (FromJSON, ToJSON) via (CustomEncoded RemoteConvMembers)
+
+-- | A conversation hosted on a remote backend. This contains the same
+-- information as a 'Conversation', with the exception that conversation status
+-- fields (muted/archived/hidden) are omitted, since they are not known by the
+-- remote backend.
+data RemoteConversation = RemoteConversation
+ { rcnvMetadata :: ConversationMetadata,
+ rcnvMembers :: RemoteConvMembers
+ }
+ deriving stock (Eq, Show, Generic)
+ deriving (Arbitrary) via (GenericUniform RemoteConversation)
+ deriving (FromJSON, ToJSON) via (CustomEncoded RemoteConversation)
+
newtype GetConversationsResponse = GetConversationsResponse
- { gcresConvs :: [Conversation]
+ { gcresConvs :: [RemoteConversation]
}
deriving stock (Eq, Show, Generic)
deriving (Arbitrary) via (GenericUniform GetConversationsResponse)
diff --git a/libs/wire-api/src/Wire/API/Conversation.hs b/libs/wire-api/src/Wire/API/Conversation.hs
index 36a397eb65..735bd55f10 100644
--- a/libs/wire-api/src/Wire/API/Conversation.hs
+++ b/libs/wire-api/src/Wire/API/Conversation.hs
@@ -23,7 +23,18 @@
-- modules.
module Wire.API.Conversation
( -- * Conversation
+ ConversationMetadata (..),
Conversation (..),
+ mkConversation,
+ cnvQualifiedId,
+ cnvType,
+ cnvCreator,
+ cnvAccess,
+ cnvAccessRole,
+ cnvName,
+ cnvTeam,
+ cnvMessageTimer,
+ cnvReceiptMode,
ConversationCoverView (..),
ConversationList (..),
ListConversations (..),
@@ -80,6 +91,7 @@ import Control.Applicative
import Control.Lens (at, (?~))
import Data.Aeson (FromJSON (..), ToJSON (..))
import qualified Data.Aeson as A
+import qualified Data.Aeson.Types as A
import qualified Data.Attoparsec.ByteString as AB
import qualified Data.ByteString as BS
import Data.Id
@@ -105,62 +117,125 @@ import Wire.API.Conversation.Role (RoleName, roleNameWireAdmin)
--------------------------------------------------------------------------------
-- Conversation
+data ConversationMetadata = ConversationMetadata
+ { -- | A qualified conversation ID
+ cnvmQualifiedId :: Qualified ConvId,
+ cnvmType :: ConvType,
+ -- FUTUREWORK: Make this a qualified user ID.
+ cnvmCreator :: UserId,
+ cnvmAccess :: [Access],
+ cnvmAccessRole :: AccessRole,
+ cnvmName :: Maybe Text,
+ -- FUTUREWORK: Think if it makes sense to make the team ID qualified due to
+ -- federation.
+ cnvmTeam :: Maybe TeamId,
+ cnvmMessageTimer :: Maybe Milliseconds,
+ cnvmReceiptMode :: Maybe ReceiptMode
+ }
+ deriving stock (Eq, Show, Generic)
+ deriving (Arbitrary) via (GenericUniform ConversationMetadata)
+ deriving (FromJSON, ToJSON) via Schema ConversationMetadata
+
+conversationMetadataObjectSchema ::
+ SchemaP
+ SwaggerDoc
+ A.Object
+ [A.Pair]
+ ConversationMetadata
+ ConversationMetadata
+conversationMetadataObjectSchema =
+ ConversationMetadata
+ <$> cnvmQualifiedId .= field "qualified_id" schema
+ <* (qUnqualified . cnvmQualifiedId)
+ .= optional (field "id" (deprecatedSchema "qualified_id" schema))
+ <*> cnvmType .= field "type" schema
+ <*> cnvmCreator
+ .= fieldWithDocModifier
+ "creator"
+ (description ?~ "The creator's user ID")
+ schema
+ <*> cnvmAccess .= field "access" (array schema)
+ <*> cnvmAccessRole .= field "access_role" schema
+ <*> cnvmName .= lax (field "name" (optWithDefault A.Null schema))
+ <* const ("0.0" :: Text) .= optional (field "last_event" schema)
+ <* const ("1970-01-01T00:00:00.000Z" :: Text)
+ .= optional (field "last_event_time" schema)
+ <*> cnvmTeam .= lax (field "team" (optWithDefault A.Null schema))
+ <*> cnvmMessageTimer
+ .= lax
+ ( fieldWithDocModifier
+ "message_timer"
+ (description ?~ "Per-conversation message timer (can be null)")
+ (optWithDefault A.Null schema)
+ )
+ <*> cnvmReceiptMode .= lax (field "receipt_mode" (optWithDefault A.Null schema))
+
+instance ToSchema ConversationMetadata where
+ schema = object "ConversationMetadata" conversationMetadataObjectSchema
+
-- | Public-facing conversation type. Represents information that a
-- particular user is allowed to see.
--
-- Can be produced from the internal one ('Galley.Data.Types.Conversation')
-- by using 'Galley.API.Mapping.conversationView'.
data Conversation = Conversation
- { -- | A qualified conversation ID
- cnvQualifiedId :: Qualified ConvId,
- cnvType :: ConvType,
- -- FUTUREWORK: Make this a qualified user ID.
- cnvCreator :: UserId,
- cnvAccess :: [Access],
- cnvAccessRole :: AccessRole,
- cnvName :: Maybe Text,
- cnvMembers :: ConvMembers,
- -- FUTUREWORK: Think if it makes sense to make the team ID qualified due to
- -- federation.
- cnvTeam :: Maybe TeamId,
- cnvMessageTimer :: Maybe Milliseconds,
- cnvReceiptMode :: Maybe ReceiptMode
+ { cnvMetadata :: ConversationMetadata,
+ cnvMembers :: ConvMembers
}
deriving stock (Eq, Show, Generic)
deriving (Arbitrary) via (GenericUniform Conversation)
deriving (FromJSON, ToJSON, S.ToSchema) via Schema Conversation
+mkConversation ::
+ Qualified ConvId ->
+ ConvType ->
+ UserId ->
+ [Access] ->
+ AccessRole ->
+ Maybe Text ->
+ ConvMembers ->
+ Maybe TeamId ->
+ Maybe Milliseconds ->
+ Maybe ReceiptMode ->
+ Conversation
+mkConversation qid ty uid acc role name mems tid ms rm =
+ Conversation (ConversationMetadata qid ty uid acc role name tid ms rm) mems
+
+cnvQualifiedId :: Conversation -> Qualified ConvId
+cnvQualifiedId = cnvmQualifiedId . cnvMetadata
+
+cnvType :: Conversation -> ConvType
+cnvType = cnvmType . cnvMetadata
+
+cnvCreator :: Conversation -> UserId
+cnvCreator = cnvmCreator . cnvMetadata
+
+cnvAccess :: Conversation -> [Access]
+cnvAccess = cnvmAccess . cnvMetadata
+
+cnvAccessRole :: Conversation -> AccessRole
+cnvAccessRole = cnvmAccessRole . cnvMetadata
+
+cnvName :: Conversation -> Maybe Text
+cnvName = cnvmName . cnvMetadata
+
+cnvTeam :: Conversation -> Maybe TeamId
+cnvTeam = cnvmTeam . cnvMetadata
+
+cnvMessageTimer :: Conversation -> Maybe Milliseconds
+cnvMessageTimer = cnvmMessageTimer . cnvMetadata
+
+cnvReceiptMode :: Conversation -> Maybe ReceiptMode
+cnvReceiptMode = cnvmReceiptMode . cnvMetadata
+
instance ToSchema Conversation where
schema =
objectWithDocModifier
"Conversation"
(description ?~ "A conversation object as returned from the server")
$ Conversation
- <$> cnvQualifiedId .= field "qualified_id" schema
- <* (qUnqualified . cnvQualifiedId)
- .= optional (field "id" (deprecatedSchema "qualified_id" schema))
- <*> cnvType .= field "type" schema
- <*> cnvCreator
- .= fieldWithDocModifier
- "creator"
- (description ?~ "The creator's user ID")
- schema
- <*> cnvAccess .= field "access" (array schema)
- <*> cnvAccessRole .= field "access_role" schema
- <*> cnvName .= lax (field "name" (optWithDefault A.Null schema))
+ <$> cnvMetadata .= conversationMetadataObjectSchema
<*> cnvMembers .= field "members" schema
- <* const ("0.0" :: Text) .= optional (field "last_event" schema)
- <* const ("1970-01-01T00:00:00.000Z" :: Text)
- .= optional (field "last_event_time" schema)
- <*> cnvTeam .= lax (field "team" (optWithDefault A.Null schema))
- <*> cnvMessageTimer
- .= lax
- ( fieldWithDocModifier
- "message_timer"
- (description ?~ "Per-conversation message timer (can be null)")
- (optWithDefault A.Null schema)
- )
- <*> cnvReceiptMode .= lax (field "receipt_mode" (optWithDefault A.Null schema))
modelConversation :: Doc.Model
modelConversation = Doc.defineModel "Conversation" $ do
diff --git a/libs/wire-api/test/unit/Test/Wire/API/Golden/Generated/ConversationList_20Conversation_user.hs b/libs/wire-api/test/unit/Test/Wire/API/Golden/Generated/ConversationList_20Conversation_user.hs
index 02ab9eb3ba..d23b8022f3 100644
--- a/libs/wire-api/test/unit/Test/Wire/API/Golden/Generated/ConversationList_20Conversation_user.hs
+++ b/libs/wire-api/test/unit/Test/Wire/API/Golden/Generated/ConversationList_20Conversation_user.hs
@@ -25,28 +25,6 @@ import Data.Qualified (Qualified (..))
import qualified Data.UUID as UUID (fromString)
import Imports (Bool (False, True), Maybe (Just, Nothing), fromJust)
import Wire.API.Conversation
- ( AccessRole
- ( PrivateAccessRole
- ),
- ConvMembers (ConvMembers, cmOthers, cmSelf),
- ConvType (RegularConv),
- Conversation (..),
- ConversationList (..),
- Member
- ( Member,
- memConvRoleName,
- memHidden,
- memHiddenRef,
- memId,
- memOtrArchived,
- memOtrArchivedRef,
- memOtrMutedRef,
- memOtrMutedStatus,
- memService
- ),
- MutedStatus (MutedStatus, fromMutedStatus),
- ReceiptMode (ReceiptMode, unReceiptMode),
- )
import Wire.API.Conversation.Role (parseRoleName)
testObject_ConversationList_20Conversation_user_1 :: ConversationList Conversation
@@ -54,12 +32,18 @@ testObject_ConversationList_20Conversation_user_1 =
ConversationList
{ convList =
[ Conversation
- { cnvQualifiedId = Qualified (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000000"))) (Domain "golden.example.com"),
- cnvType = RegularConv,
- cnvCreator = Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000001")),
- cnvAccess = [],
- cnvAccessRole = PrivateAccessRole,
- cnvName = Just "",
+ { cnvMetadata =
+ ConversationMetadata
+ { cnvmQualifiedId = Qualified (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000000"))) (Domain "golden.example.com"),
+ cnvmType = RegularConv,
+ cnvmCreator = Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000001")),
+ cnvmAccess = [],
+ cnvmAccessRole = PrivateAccessRole,
+ cnvmName = Just "",
+ cnvmTeam = Just (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000001"))),
+ cnvmMessageTimer = Just (Ms {ms = 4760386328981119}),
+ cnvmReceiptMode = Just (ReceiptMode {unReceiptMode = 0})
+ },
cnvMembers =
ConvMembers
{ cmSelf =
@@ -76,10 +60,7 @@ testObject_ConversationList_20Conversation_user_1 =
fromJust (parseRoleName "71xuphsrwfoktrpiv4d08dxj6_1umizg67iisctw87gemvi114mtu")
},
cmOthers = []
- },
- cnvTeam = Just (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000001"))),
- cnvMessageTimer = Just (Ms {ms = 4760386328981119}),
- cnvReceiptMode = Just (ReceiptMode {unReceiptMode = 0})
+ }
}
],
convHasMore = False
diff --git a/libs/wire-api/test/unit/Test/Wire/API/Golden/Generated/Conversation_user.hs b/libs/wire-api/test/unit/Test/Wire/API/Golden/Generated/Conversation_user.hs
index c59d97a208..0ea5ad8e86 100644
--- a/libs/wire-api/test/unit/Test/Wire/API/Golden/Generated/Conversation_user.hs
+++ b/libs/wire-api/test/unit/Test/Wire/API/Golden/Generated/Conversation_user.hs
@@ -34,12 +34,18 @@ domain = Domain "golden.example.com"
testObject_Conversation_user_1 :: Conversation
testObject_Conversation_user_1 =
Conversation
- { cnvQualifiedId = Qualified (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000000"))) (Domain "golden.example.com"),
- cnvType = One2OneConv,
- cnvCreator = Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000200000001")),
- cnvAccess = [],
- cnvAccessRole = PrivateAccessRole,
- cnvName = Just " 0",
+ { cnvMetadata =
+ ConversationMetadata
+ { cnvmQualifiedId = Qualified (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000000"))) (Domain "golden.example.com"),
+ cnvmType = One2OneConv,
+ cnvmCreator = Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000200000001")),
+ cnvmAccess = [],
+ cnvmAccessRole = PrivateAccessRole,
+ cnvmName = Just " 0",
+ cnvmTeam = Just (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000002"))),
+ cnvmMessageTimer = Nothing,
+ cnvmReceiptMode = Just (ReceiptMode {unReceiptMode = -2})
+ },
cnvMembers =
ConvMembers
{ cmSelf =
@@ -55,34 +61,37 @@ testObject_Conversation_user_1 =
memConvRoleName = fromJust (parseRoleName "rhhdzf0j0njilixx0g0vzrp06b_5us")
},
cmOthers = []
- },
- cnvTeam = Just (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000002"))),
- cnvMessageTimer = Nothing,
- cnvReceiptMode = Just (ReceiptMode {unReceiptMode = -2})
+ }
}
testObject_Conversation_user_2 :: Conversation
testObject_Conversation_user_2 =
Conversation
- { cnvQualifiedId = Qualified (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000002"))) (Domain "golden.example.com"),
- cnvType = SelfConv,
- cnvCreator = Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000200000001")),
- cnvAccess =
- [ InviteAccess,
- InviteAccess,
- CodeAccess,
- LinkAccess,
- InviteAccess,
- PrivateAccess,
- LinkAccess,
- CodeAccess,
- CodeAccess,
- LinkAccess,
- PrivateAccess,
- InviteAccess
- ],
- cnvAccessRole = NonActivatedAccessRole,
- cnvName = Just "",
+ { cnvMetadata =
+ ConversationMetadata
+ { cnvmQualifiedId = Qualified (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000002"))) (Domain "golden.example.com"),
+ cnvmType = SelfConv,
+ cnvmCreator = Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000200000001")),
+ cnvmAccess =
+ [ InviteAccess,
+ InviteAccess,
+ CodeAccess,
+ LinkAccess,
+ InviteAccess,
+ PrivateAccess,
+ LinkAccess,
+ CodeAccess,
+ CodeAccess,
+ LinkAccess,
+ PrivateAccess,
+ InviteAccess
+ ],
+ cnvmAccessRole = NonActivatedAccessRole,
+ cnvmName = Just "",
+ cnvmTeam = Nothing,
+ cnvmMessageTimer = Just (Ms {ms = 1319272593797015}),
+ cnvmReceiptMode = Nothing
+ },
cnvMembers =
ConvMembers
{ cmSelf =
@@ -117,8 +126,5 @@ testObject_Conversation_user_2 =
)
}
]
- },
- cnvTeam = Nothing,
- cnvMessageTimer = Just (Ms {ms = 1319272593797015}),
- cnvReceiptMode = 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 ae6cb47adb..cdd9a029b2 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
@@ -28,51 +28,10 @@ import Data.Text.Ascii (validate)
import qualified Data.UUID as UUID (fromString)
import Imports (Bool (False, True), Maybe (Just, Nothing), fromJust, read, undefined)
import Wire.API.Conversation
-import Wire.API.Conversation.Code (ConversationCode (..), Key (..), Value (..))
+import Wire.API.Conversation.Code (Key (..), Value (..))
import Wire.API.Conversation.Role (parseRoleName)
-import Wire.API.Conversation.Typing (TypingData (TypingData, tdStatus), TypingStatus (StoppedTyping))
+import Wire.API.Conversation.Typing (TypingStatus (..))
import Wire.API.Event.Conversation
- ( Connect (Connect, cEmail, cMessage, cName, cRecipient),
- Event (Event),
- EventData (..),
- EventType
- ( ConvAccessUpdate,
- ConvCodeDelete,
- ConvCodeUpdate,
- ConvConnect,
- ConvCreate,
- ConvDelete,
- ConvMessageTimerUpdate,
- ConvReceiptModeUpdate,
- ConvRename,
- MemberJoin,
- MemberLeave,
- MemberStateUpdate,
- OtrMessageAdd,
- Typing
- ),
- MemberUpdateData
- ( MemberUpdateData,
- misConvRoleName,
- misHidden,
- misHiddenRef,
- misOtrArchived,
- misOtrArchivedRef,
- misOtrMutedRef,
- misOtrMutedStatus,
- misTarget
- ),
- OtrMessage
- ( OtrMessage,
- otrCiphertext,
- otrData,
- otrRecipient,
- otrSender
- ),
- QualifiedUserIdList (QualifiedUserIdList, qualifiedUserIdList),
- SimpleMember (..),
- SimpleMembers (SimpleMembers, mMembers),
- )
import Wire.API.Provider.Service (ServiceRef (ServiceRef, _serviceRefId, _serviceRefProvider))
domain :: Domain
@@ -180,13 +139,19 @@ testObject_Event_user_8 =
(read "1864-05-29 19:31:31.226 UTC")
( EdConversation
( Conversation
- { cnvQualifiedId = Qualified (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000001"))) (Domain "golden.example.com"),
- cnvType = RegularConv,
- cnvCreator = Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000200000001")),
- cnvAccess =
- [InviteAccess, PrivateAccess, LinkAccess, InviteAccess, InviteAccess, InviteAccess, LinkAccess],
- cnvAccessRole = NonActivatedAccessRole,
- cnvName = Just "\a\SO\r",
+ { cnvMetadata =
+ ConversationMetadata
+ { cnvmQualifiedId = Qualified (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000001"))) (Domain "golden.example.com"),
+ cnvmType = RegularConv,
+ cnvmCreator = Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000200000001")),
+ cnvmAccess =
+ [InviteAccess, PrivateAccess, LinkAccess, InviteAccess, InviteAccess, InviteAccess, LinkAccess],
+ cnvmAccessRole = NonActivatedAccessRole,
+ cnvmName = Just "\a\SO\r",
+ cnvmTeam = Just (Id (fromJust (UUID.fromString "00000000-0000-0002-0000-000100000001"))),
+ cnvmMessageTimer = Just (Ms {ms = 283898987885780}),
+ cnvmReceiptMode = Just (ReceiptMode {unReceiptMode = -1})
+ },
cnvMembers =
ConvMembers
{ cmSelf =
@@ -223,10 +188,7 @@ testObject_Event_user_8 =
)
}
]
- },
- cnvTeam = Just (Id (fromJust (UUID.fromString "00000000-0000-0002-0000-000100000001"))),
- cnvMessageTimer = Just (Ms {ms = 283898987885780}),
- cnvReceiptMode = Just (ReceiptMode {unReceiptMode = -1})
+ }
}
)
)
diff --git a/libs/wire-api/test/unit/Test/Wire/API/Golden/Manual/ConversationsResponse.hs b/libs/wire-api/test/unit/Test/Wire/API/Golden/Manual/ConversationsResponse.hs
index 306222effb..f91466f0dc 100644
--- a/libs/wire-api/test/unit/Test/Wire/API/Golden/Manual/ConversationsResponse.hs
+++ b/libs/wire-api/test/unit/Test/Wire/API/Golden/Manual/ConversationsResponse.hs
@@ -29,12 +29,18 @@ testObject_ConversationsResponse_1 =
conv1 :: Conversation
conv1 =
Conversation
- { cnvQualifiedId = Qualified (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000000"))) (Domain "golden.example.com"),
- cnvType = One2OneConv,
- cnvCreator = Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000200000001")),
- cnvAccess = [],
- cnvAccessRole = PrivateAccessRole,
- cnvName = Just " 0",
+ { cnvMetadata =
+ ConversationMetadata
+ { cnvmQualifiedId = Qualified (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000000"))) (Domain "golden.example.com"),
+ cnvmType = One2OneConv,
+ cnvmCreator = Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000200000001")),
+ cnvmAccess = [],
+ cnvmAccessRole = PrivateAccessRole,
+ cnvmName = Just " 0",
+ cnvmTeam = Just (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000002"))),
+ cnvmMessageTimer = Nothing,
+ cnvmReceiptMode = Just (ReceiptMode {unReceiptMode = -2})
+ },
cnvMembers =
ConvMembers
{ cmSelf =
@@ -50,34 +56,37 @@ conv1 =
memConvRoleName = fromJust (parseRoleName "rhhdzf0j0njilixx0g0vzrp06b_5us")
},
cmOthers = []
- },
- cnvTeam = Just (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000002"))),
- cnvMessageTimer = Nothing,
- cnvReceiptMode = Just (ReceiptMode {unReceiptMode = -2})
+ }
}
conv2 :: Conversation
conv2 =
Conversation
- { cnvQualifiedId = Qualified (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000002"))) (Domain "golden.example.com"),
- cnvType = SelfConv,
- cnvCreator = Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000200000001")),
- cnvAccess =
- [ InviteAccess,
- InviteAccess,
- CodeAccess,
- LinkAccess,
- InviteAccess,
- PrivateAccess,
- LinkAccess,
- CodeAccess,
- CodeAccess,
- LinkAccess,
- PrivateAccess,
- InviteAccess
- ],
- cnvAccessRole = NonActivatedAccessRole,
- cnvName = Just "",
+ { cnvMetadata =
+ ConversationMetadata
+ { cnvmQualifiedId = Qualified (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000002"))) (Domain "golden.example.com"),
+ cnvmType = SelfConv,
+ cnvmCreator = Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000200000001")),
+ cnvmAccess =
+ [ InviteAccess,
+ InviteAccess,
+ CodeAccess,
+ LinkAccess,
+ InviteAccess,
+ PrivateAccess,
+ LinkAccess,
+ CodeAccess,
+ CodeAccess,
+ LinkAccess,
+ PrivateAccess,
+ InviteAccess
+ ],
+ cnvmAccessRole = NonActivatedAccessRole,
+ cnvmName = Just "",
+ cnvmTeam = Just (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000200000000"))),
+ cnvmMessageTimer = Just (Ms {ms = 1319272593797015}),
+ cnvmReceiptMode = Just (ReceiptMode {unReceiptMode = 2})
+ },
cnvMembers =
ConvMembers
{ cmSelf =
@@ -94,8 +103,5 @@ conv2 =
fromJust (parseRoleName "9b2d3thyqh4ptkwtq2n2v9qsni_ln1ca66et_z8dlhfs9oamp328knl3rj9kcj")
},
cmOthers = []
- },
- cnvTeam = Just (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000200000000"))),
- cnvMessageTimer = Just (Ms {ms = 1319272593797015}),
- cnvReceiptMode = Just (ReceiptMode {unReceiptMode = 2})
+ }
}
diff --git a/services/brig/src/Brig/Provider/API.hs b/services/brig/src/Brig/Provider/API.hs
index 55ba100025..ad2513d571 100644
--- a/services/brig/src/Brig/Provider/API.hs
+++ b/services/brig/src/Brig/Provider/API.hs
@@ -73,7 +73,7 @@ import qualified Data.Set as Set
import qualified Data.Swagger.Build.Api as Doc
import qualified Data.Text.Ascii as Ascii
import qualified Data.Text.Encoding as Text
-import Galley.Types (AccessRole (..), ConvMembers (..), ConvType (..), Conversation (..), OtherMember (..))
+import Galley.Types
import Galley.Types.Bot (newServiceRef, serviceRefId, serviceRefProvider)
import Galley.Types.Conversations.Roles (roleNameWireAdmin)
import qualified Galley.Types.Teams as Teams
diff --git a/services/brig/test/integration/API/Provider.hs b/services/brig/test/integration/API/Provider.hs
index 277058debf..7af5a32541 100644
--- a/services/brig/test/integration/API/Provider.hs
+++ b/services/brig/test/integration/API/Provider.hs
@@ -66,7 +66,7 @@ import Data.Time.Clock
import Data.Timeout (TimedOut (..), Timeout, TimeoutUnit (..), (#))
import qualified Data.UUID as UUID
import qualified Data.ZAuth.Token as ZAuth
-import Galley.Types (Access (..), AccessRole (..), ConvMembers (..), Conversation (..), ConversationAccessUpdate (..), Event (..), EventData (..), EventType (..), NewConv (..), NewConvUnmanaged (..), OtherMember (..), OtrMessage (..), QualifiedUserIdList (..), SimpleMember (..), SimpleMembers (..))
+import Galley.Types
import Galley.Types.Bot (ServiceRef, newServiceRef, serviceRefId, serviceRefProvider)
import Galley.Types.Conversations.Roles (roleNameWireAdmin)
import qualified Galley.Types.Teams as Team
diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal
index 0357c2adca..5122ed04e0 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: 0acb724202f4ba39242c1ebbe5f5db555404624b7a6be922d5a4148d38c5786d
+-- hash: 0341ec52f506f40a39b7329c4eeccdccf25bcffc81318f535602bfc17e655f58
name: galley
version: 0.83.0
@@ -176,6 +176,7 @@ executable galley
, raw-strings-qq >=1.0
, safe >=0.3
, ssl-util
+ , tagged
, types-common
, wire-api
, wire-api-federation
@@ -314,6 +315,7 @@ executable galley-migrate-data
, raw-strings-qq >=1.0
, safe >=0.3
, ssl-util
+ , tagged
, text
, time
, tinylog
@@ -361,6 +363,7 @@ executable galley-schema
V50_AddLegalholdWhitelisted
V51_FeatureFileSharing
V52_FeatureConferenceCalling
+ V53_AddRemoteConvStatus
Paths_galley
hs-source-dirs:
schema/src
@@ -376,6 +379,7 @@ executable galley-schema
, raw-strings-qq >=1.0
, safe >=0.3
, ssl-util
+ , tagged
, text
, tinylog
, wire-api
@@ -413,6 +417,7 @@ test-suite galley-types-tests
, safe >=0.3
, servant-swagger
, ssl-util
+ , tagged
, tasty
, tasty-hspec
, tasty-hunit
diff --git a/services/galley/package.yaml b/services/galley/package.yaml
index 795fe273d0..0ba2b59e4f 100644
--- a/services/galley/package.yaml
+++ b/services/galley/package.yaml
@@ -18,6 +18,7 @@ dependencies:
- raw-strings-qq >=1.0
- wire-api
- wire-api-federation
+- tagged
library:
source-dirs: src
@@ -76,7 +77,6 @@ library:
- string-conversions
- swagger >=0.1
- swagger2
- - tagged
- text >=0.11
- time >=1.4
- tinylog >=0.10
diff --git a/services/galley/schema/src/Main.hs b/services/galley/schema/src/Main.hs
index 5d2b408c42..c350df9f4d 100644
--- a/services/galley/schema/src/Main.hs
+++ b/services/galley/schema/src/Main.hs
@@ -55,6 +55,7 @@ import qualified V49_ReAddRemoteIdentifiers
import qualified V50_AddLegalholdWhitelisted
import qualified V51_FeatureFileSharing
import qualified V52_FeatureConferenceCalling
+import qualified V53_AddRemoteConvStatus
main :: IO ()
main = do
@@ -95,7 +96,8 @@ main = do
V49_ReAddRemoteIdentifiers.migration,
V50_AddLegalholdWhitelisted.migration,
V51_FeatureFileSharing.migration,
- V52_FeatureConferenceCalling.migration
+ V52_FeatureConferenceCalling.migration,
+ V53_AddRemoteConvStatus.migration
-- When adding migrations here, don't forget to update
-- 'schemaVersion' in Galley.Data
-- (see also docs/developer/cassandra-interaction.md)
diff --git a/services/galley/schema/src/V53_AddRemoteConvStatus.hs b/services/galley/schema/src/V53_AddRemoteConvStatus.hs
new file mode 100644
index 0000000000..0688e82493
--- /dev/null
+++ b/services/galley/schema/src/V53_AddRemoteConvStatus.hs
@@ -0,0 +1,38 @@
+-- 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 V53_AddRemoteConvStatus (migration) where
+
+import Cassandra.Schema
+import Imports
+import Text.RawString.QQ
+
+-- This migration adds fields that track remote conversation status for a local user.
+migration :: Migration
+migration =
+ Migration 53 "Add fields for remote conversation status (hidden/archived/muted)" $
+ schema'
+ [r|
+ ALTER TABLE user_remote_conv ADD (
+ hidden boolean,
+ hidden_ref text,
+ otr_archived boolean,
+ otr_archived_ref text,
+ otr_muted_status int,
+ otr_muted_ref text
+ )
+ |]
diff --git a/services/galley/src/Galley/API/Create.hs b/services/galley/src/Galley/API/Create.hs
index 52921e4e5a..72f02735da 100644
--- a/services/galley/src/Galley/API/Create.hs
+++ b/services/galley/src/Galley/API/Create.hs
@@ -332,7 +332,7 @@ notifyCreatedConversation dtime usr conn c = do
toPush dom t m = do
let qconv = Qualified (Data.convId c) dom
qusr = Qualified usr dom
- c' <- conversationView (memId m) c
+ c' <- conversationView (lmId m) c
let e = Event ConvCreate qconv qusr t (EdConversation c')
return $
newPushLocal1 ListComplete usr (ConvEvent e) (list1 (recipient m) [])
diff --git a/services/galley/src/Galley/API/Federation.hs b/services/galley/src/Galley/API/Federation.hs
index f8e42b7548..414e36d421 100644
--- a/services/galley/src/Galley/API/Federation.hs
+++ b/services/galley/src/Galley/API/Federation.hs
@@ -27,7 +27,7 @@ import Data.Json.Util (Base64ByteString (..))
import Data.List1 (list1)
import qualified Data.Map as Map
import Data.Map.Lens (toMapOf)
-import Data.Qualified (Qualified (..))
+import Data.Qualified (Qualified (..), toRemote)
import qualified Data.Set as Set
import Data.Tagged
import qualified Data.Text.Lazy as LT
@@ -38,7 +38,7 @@ import qualified Galley.API.Update as API
import Galley.API.Util (fromNewRemoteConversation, pushConversationEvent, viewFederationDomain)
import Galley.App (Galley)
import qualified Galley.Data as Data
-import Galley.Types.Conversations.Members (InternalMember (..), LocalMember)
+import Galley.Types.Conversations.Members (LocalMember (..), defMemberStatus)
import Imports
import Servant (ServerT)
import Servant.API.Generic (ToServantApi)
@@ -96,12 +96,14 @@ onConversationCreated domain rc = do
(EdConversation c)
pushConversationEvent Nothing event [Public.memId mem] []
-getConversations :: GetConversationsRequest -> Galley GetConversationsResponse
-getConversations (GetConversationsRequest qUid gcrConvIds) = do
- domain <- viewFederationDomain
- convs <- Data.conversations gcrConvIds
- let convViews = Mapping.conversationViewMaybeQualified domain qUid <$> convs
- pure $ GetConversationsResponse . catMaybes $ convViews
+getConversations :: Domain -> GetConversationsRequest -> Galley GetConversationsResponse
+getConversations domain (GetConversationsRequest uid cids) = do
+ let ruid = toRemote $ Qualified uid domain
+ localDomain <- viewFederationDomain
+ GetConversationsResponse
+ . catMaybes
+ . map (Mapping.conversationToRemote localDomain ruid)
+ <$> Data.conversations cids
-- | Update the local database with information on conversation members joining
-- or leaving. Finally, push out notifications to local users.
@@ -186,16 +188,11 @@ onMessageSent domain rmUnqualified = do
mkLocalMember :: UserId -> Galley LocalMember
mkLocalMember m =
pure $
- InternalMember
- { memId = m,
- memService = Nothing,
- memOtrMutedStatus = Nothing,
- memOtrMutedRef = Nothing,
- memOtrArchived = False,
- memOtrArchivedRef = Nothing,
- memHidden = False,
- memHiddenRef = Nothing,
- memConvRoleName = Public.roleNameWireMember
+ LocalMember
+ { lmId = m,
+ lmService = Nothing,
+ lmStatus = defMemberStatus,
+ lmConvRoleName = Public.roleNameWireMember
}
sendMessage :: Domain -> MessageSendRequest -> Galley MessageSendResponse
diff --git a/services/galley/src/Galley/API/LegalHold.hs b/services/galley/src/Galley/API/LegalHold.hs
index d4f9120986..5a4bd6d8c3 100644
--- a/services/galley/src/Galley/API/LegalHold.hs
+++ b/services/galley/src/Galley/API/LegalHold.hs
@@ -61,7 +61,7 @@ import qualified Galley.External.LegalHoldService as LHService
import qualified Galley.Intra.Client as Client
import Galley.Intra.User (getConnections, putConnectionInternal)
import qualified Galley.Options as Opts
-import Galley.Types (LocalMember, memConvRoleName, memId)
+import Galley.Types (LocalMember, lmConvRoleName, lmId)
import Galley.Types.Teams as Team
import Imports
import Network.HTTP.Types (status200, status404)
@@ -492,12 +492,12 @@ handleGroupConvPolicyConflicts uid hypotheticalLHStatus =
membersAndLHStatus :: [(LocalMember, UserLegalHoldStatus)] <- do
let mems = Data.convLocalMembers conv
- uidsLHStatus <- getLHStatusForUsers (memId <$> mems)
+ uidsLHStatus <- getLHStatusForUsers (lmId <$> mems)
pure $
zipWith
( \mem (mid, status) ->
- assert (memId mem == mid) $
- if memId mem == uid
+ assert (lmId mem == mid) $
+ if lmId mem == uid
then (mem, hypotheticalLHStatus)
else (mem, status)
)
@@ -507,10 +507,10 @@ handleGroupConvPolicyConflicts uid hypotheticalLHStatus =
let qconv = Data.convId conv `Qualified` localDomain
if any
((== ConsentGiven) . consentGiven . snd)
- (filter ((== roleNameWireAdmin) . memConvRoleName . fst) membersAndLHStatus)
+ (filter ((== roleNameWireAdmin) . lmConvRoleName . fst) membersAndLHStatus)
then do
for_ (filter ((== ConsentNotGiven) . consentGiven . snd) membersAndLHStatus) $ \(memberNoConsent, _) -> do
- removeMember (memId memberNoConsent `Qualified` localDomain) Nothing qconv (Qualified (memId memberNoConsent) localDomain)
+ removeMember (lmId memberNoConsent `Qualified` localDomain) Nothing qconv (Qualified (lmId memberNoConsent) localDomain)
else do
for_ (filter (userLHEnabled . snd) membersAndLHStatus) $ \(legalholder, _) -> do
- removeMember (memId legalholder `Qualified` localDomain) Nothing qconv (Qualified (memId legalholder) localDomain)
+ removeMember (lmId legalholder `Qualified` localDomain) Nothing qconv (Qualified (lmId legalholder) localDomain)
diff --git a/services/galley/src/Galley/API/Mapping.hs b/services/galley/src/Galley/API/Mapping.hs
index 4eb47d23cb..e99921917e 100644
--- a/services/galley/src/Galley/API/Mapping.hs
+++ b/services/galley/src/Galley/API/Mapping.hs
@@ -17,31 +17,39 @@
-- You should have received a copy of the GNU Affero General Public License along
-- with this program. If not, see .
-module Galley.API.Mapping where
+module Galley.API.Mapping
+ ( conversationView,
+ conversationViewMaybe,
+ remoteConversationView,
+ conversationToRemote,
+ localMemberToSelf,
+ )
+where
import Control.Monad.Catch
import Data.Domain (Domain)
import Data.Id (UserId, idToText)
-import qualified Data.List as List
-import Data.Qualified (Qualified (..))
-import Data.Tagged (unTagged)
+import Data.Qualified
import Galley.API.Util (viewFederationDomain)
import Galley.App
import qualified Galley.Data as Data
import Galley.Data.Types (convId)
-import qualified Galley.Types.Conversations.Members as Internal
+import Galley.Types.Conversations.Members
import Imports
import Network.HTTP.Types.Status
import Network.Wai.Utilities.Error
import qualified System.Logger.Class as Log
import System.Logger.Message (msg, val, (+++))
-import qualified Wire.API.Conversation as Public
+import Wire.API.Conversation
+import Wire.API.Federation.API.Galley
-- | View for a given user of a stored conversation.
+--
-- Throws "bad-state" when the user is not part of the conversation.
-conversationView :: UserId -> Data.Conversation -> Galley Public.Conversation
+conversationView :: UserId -> Data.Conversation -> Galley Conversation
conversationView uid conv = do
- mbConv <- conversationViewMaybe uid conv
+ localDomain <- viewFederationDomain
+ let mbConv = conversationViewMaybe localDomain uid conv
maybe memberNotFound pure mbConv
where
memberNotFound = do
@@ -53,73 +61,85 @@ conversationView uid conv = do
throwM badState
badState = mkError status500 "bad-state" "Bad internal member state."
-conversationViewMaybe :: UserId -> Data.Conversation -> Galley (Maybe Public.Conversation)
-conversationViewMaybe u conv = do
- localDomain <- viewFederationDomain
- pure $ conversationViewMaybeQualified localDomain (Qualified u localDomain) conv
-
-- | View for a given user of a stored conversation.
--- Returns 'Nothing' when the user is not part of the conversation.
-conversationViewMaybeQualified :: Domain -> Qualified UserId -> Data.Conversation -> Maybe Public.Conversation
-conversationViewMaybeQualified localDomain qUid Data.Conversation {..} = do
- let localMembers = localToOther localDomain <$> convLocalMembers
- let remoteMembers = remoteToOther <$> convRemoteMembers
- let me = List.find ((qUid ==) . Public.omQualifiedId) (localMembers <> remoteMembers)
- let otherMembers = filter ((qUid /=) . Public.omQualifiedId) (localMembers <> remoteMembers)
- let userAndConvOnSameBackend = find ((qUnqualified qUid ==) . Internal.memId) convLocalMembers
- let selfMember =
- -- if the user and the conversation are on the same backend, we can create a real self member
- -- otherwise, we need to fall back to a default self member (see futurework)
- -- (Note: the extra domain check is done to catch the edge case where two users in a conversation have the same unqualified UUID)
- if isJust userAndConvOnSameBackend && localDomain == qDomain qUid
- then toMember <$> userAndConvOnSameBackend
- else incompleteSelfMember <$> me
- selfMember <&> \m -> do
- let mems = Public.ConvMembers m otherMembers
- Public.Conversation
- (Qualified convId localDomain)
- convType
- convCreator
- convAccess
- convAccessRole
- convName
- mems
- convTeam
- convMessageTimer
- convReceiptMode
- where
- localToOther :: Domain -> Internal.LocalMember -> Public.OtherMember
- localToOther domain x =
- Public.OtherMember
- { Public.omQualifiedId = Qualified (Internal.memId x) domain,
- Public.omService = Internal.memService x,
- Public.omConvRoleName = Internal.memConvRoleName x
- }
+--
+-- Returns 'Nothing' if the user is not part of the conversation.
+conversationViewMaybe :: Domain -> UserId -> Data.Conversation -> Maybe Conversation
+conversationViewMaybe localDomain uid conv = do
+ let (selfs, lothers) = partition ((uid ==) . lmId) (Data.convLocalMembers conv)
+ rothers = Data.convRemoteMembers conv
+ self <- localMemberToSelf <$> listToMaybe selfs
+ let others =
+ map (localMemberToOther localDomain) lothers
+ <> map remoteMemberToOther rothers
+ pure $
+ Conversation
+ (Data.convMetadata localDomain conv)
+ (ConvMembers self others)
- remoteToOther :: Internal.RemoteMember -> Public.OtherMember
- remoteToOther x =
- Public.OtherMember
- { Public.omQualifiedId = unTagged (Internal.rmId x),
- Public.omService = Nothing,
- Public.omConvRoleName = Internal.rmConvRoleName x
- }
+-- | View for a local user of a remote conversation.
+--
+-- If the local user is not actually present in the conversation, simply
+-- discard the conversation altogether. This should only happen if the remote
+-- backend is misbehaving.
+remoteConversationView ::
+ UserId ->
+ MemberStatus ->
+ RemoteConversation ->
+ Maybe Conversation
+remoteConversationView uid status rconv = do
+ let mems = rcnvMembers rconv
+ others = rcmOthers mems
+ self =
+ localMemberToSelf
+ LocalMember
+ { lmId = uid,
+ lmService = Nothing,
+ lmStatus = status,
+ lmConvRoleName = rcmSelfRole mems
+ }
+ pure $ Conversation (rcnvMetadata rconv) (ConvMembers self others)
- -- FUTUREWORK(federation): we currently don't store muted, archived etc status for users who are on a different backend than a conversation
- -- but we should. Once this information is available, the code should be changed to use the stored information, rather than these defaults.
- incompleteSelfMember :: Public.OtherMember -> Public.Member
- incompleteSelfMember m =
- Public.Member
- { memId = qUnqualified (Public.omQualifiedId m),
- memService = Nothing,
- memOtrMutedStatus = Nothing,
- memOtrMutedRef = Nothing,
- memOtrArchived = False,
- memOtrArchivedRef = Nothing,
- memHidden = False,
- memHiddenRef = Nothing,
- memConvRoleName = Public.omConvRoleName m
- }
+-- | Convert a local conversation to a structure to be returned to a remote
+-- backend.
+--
+-- This returns 'Nothing' if the given remote user is not part of the conversation.
+conversationToRemote ::
+ Domain ->
+ Remote UserId ->
+ Data.Conversation ->
+ Maybe RemoteConversation
+conversationToRemote localDomain ruid conv = do
+ let (selfs, rothers) = partition ((== ruid) . rmId) (Data.convRemoteMembers conv)
+ lothers = Data.convLocalMembers conv
+ selfRole <- rmConvRoleName <$> listToMaybe selfs
+ let others =
+ map (localMemberToOther localDomain) lothers
+ <> map remoteMemberToOther rothers
+ pure $
+ RemoteConversation
+ { rcnvMetadata = Data.convMetadata localDomain conv,
+ rcnvMembers =
+ RemoteConvMembers
+ { rcmSelfRole = selfRole,
+ rcmOthers = others
+ }
+ }
-toMember :: Internal.LocalMember -> Public.Member
-toMember x@Internal.InternalMember {..} =
- Public.Member {memId = Internal.memId x, ..}
+-- | Convert a local conversation member (as stored in the DB) to a publicly
+-- facing 'Member' structure.
+localMemberToSelf :: LocalMember -> Member
+localMemberToSelf lm =
+ Member
+ { memId = lmId lm,
+ memService = lmService lm,
+ memOtrMutedStatus = msOtrMutedStatus st,
+ memOtrMutedRef = msOtrMutedRef st,
+ memOtrArchived = msOtrArchived st,
+ memOtrArchivedRef = msOtrArchivedRef st,
+ memHidden = msHidden st,
+ memHiddenRef = msHiddenRef st,
+ memConvRoleName = lmConvRoleName lm
+ }
+ where
+ st = lmStatus lm
diff --git a/services/galley/src/Galley/API/Message.hs b/services/galley/src/Galley/API/Message.hs
index 8b2683ecaa..ab744e355d 100644
--- a/services/galley/src/Galley/API/Message.hs
+++ b/services/galley/src/Galley/API/Message.hs
@@ -225,9 +225,9 @@ postQualifiedOtrMessage senderType sender mconn convId msg = runExceptT $ do
localMembers <- lift $ Data.members convId
remoteMembers <- Data.lookupRemoteMembers convId
- let localMemberIds = memId <$> localMembers
+ let localMemberIds = lmId <$> localMembers
localMemberMap :: Map UserId LocalMember
- localMemberMap = Map.fromList (map (\mem -> (memId mem, mem)) localMembers)
+ localMemberMap = Map.fromList (map (\mem -> (lmId mem, mem)) localMembers)
members :: Set (Qualified UserId)
members =
Set.map (`Qualified` localDomain) (Map.keysSet localMemberMap)
@@ -246,7 +246,7 @@ postQualifiedOtrMessage senderType sender mconn convId msg = runExceptT $ do
else Data.lookupClients localMemberIds
let qualifiedLocalClients =
Map.mapKeys (localDomain,)
- . makeUserMap (Set.fromList (map memId localMembers))
+ . makeUserMap (Set.fromList (map lmId localMembers))
. Clients.toMap
$ localClients
@@ -463,7 +463,7 @@ newMessagePush localDomain members mconn mm (k, client) e = fromMaybe mempty $ d
newUserMessagePush :: LocalMember -> Maybe MessagePush
newUserMessagePush member =
fmap newUserPush $
- newConversationEventPush localDomain e [memId member]
+ newConversationEventPush localDomain e [lmId member]
<&> set pushConn mconn
. set pushNativePriority (mmNativePriority mm)
. set pushRoute (bool RouteDirect RouteAny (mmNativePush mm))
diff --git a/services/galley/src/Galley/API/Public.hs b/services/galley/src/Galley/API/Public.hs
index 6fb84c5664..191d37ccc2 100644
--- a/services/galley/src/Galley/API/Public.hs
+++ b/services/galley/src/Galley/API/Public.hs
@@ -96,7 +96,7 @@ servantSitemap =
GalleyAPI.updateConversationNameUnqualified = Update.updateLocalConversationName,
GalleyAPI.updateConversationName = Update.updateConversationName,
GalleyAPI.getConversationSelfUnqualified = Query.getLocalSelf,
- GalleyAPI.updateConversationSelfUnqualified = Update.updateLocalSelfMember,
+ GalleyAPI.updateConversationSelfUnqualified = Update.updateUnqualifiedSelfMember,
GalleyAPI.updateConversationSelf = Update.updateSelfMember,
GalleyAPI.getTeamConversationRoles = Teams.getTeamConversationRoles,
GalleyAPI.getTeamConversations = Teams.getTeamConversations,
diff --git a/services/galley/src/Galley/API/Query.hs b/services/galley/src/Galley/API/Query.hs
index 7500ba068a..be37e81e6d 100644
--- a/services/galley/src/Galley/API/Query.hs
+++ b/services/galley/src/Galley/API/Query.hs
@@ -36,12 +36,13 @@ where
import qualified Cassandra as C
import Control.Monad.Catch (throwM)
-import Control.Monad.Except (ExceptT, runExceptT)
+import Control.Monad.Trans.Except
import qualified Data.ByteString.Lazy as LBS
import Data.Code
import Data.CommaSeparatedList
import Data.Domain (Domain)
import Data.Id as Id
+import qualified Data.Map as Map
import Data.Proxy
import Data.Qualified (Qualified (..), Remote, partitionRemote, partitionRemoteOrLocalIds', toRemote)
import Data.Range
@@ -54,12 +55,14 @@ import Galley.App
import qualified Galley.Data as Data
import qualified Galley.Data.Types as Data
import Galley.Types
+import Galley.Types.Conversations.Members
import Galley.Types.Conversations.Roles
import Imports
import Network.HTTP.Types
import Network.Wai
import Network.Wai.Predicate hiding (result, setStatus)
import Network.Wai.Utilities
+import qualified Network.Wai.Utilities.Error as Wai
import qualified System.Logger.Class as Logger
import UnliftIO (pooledForConcurrentlyN)
import Wire.API.Conversation (ConversationCoverView (..))
@@ -85,10 +88,10 @@ getBotConversation zbot zcnv = do
where
mkMember :: Domain -> LocalMember -> Maybe OtherMember
mkMember domain m
- | memId m == botUserId zbot =
+ | lmId m == botUserId zbot =
Nothing -- no need to list the bot itself
| otherwise =
- Just (OtherMember (Qualified (memId m) domain) (memService m) (memConvRoleName m))
+ Just (OtherMember (Qualified (lmId m) domain) (lmService m) (lmConvRoleName m))
getUnqualifiedConversation :: UserId -> ConvId -> Galley Public.Conversation
getUnqualifiedConversation zusr cnv = do
@@ -104,9 +107,6 @@ getConversation zusr cnv = do
where
getRemoteConversation :: Remote ConvId -> Galley Public.Conversation
getRemoteConversation remoteConvId = do
- foundConvs <- Data.remoteConversationIdOf zusr [remoteConvId]
- unless (remoteConvId `elem` foundConvs) $
- throwErrorDescription convNotFound
conversations <- getRemoteConversations zusr [remoteConvId]
case conversations of
[] -> throwErrorDescription convNotFound
@@ -114,39 +114,90 @@ getConversation zusr cnv = do
_convs -> throwM (federationUnexpectedBody "expected one conversation, got multiple")
getRemoteConversations :: UserId -> [Remote ConvId] -> Galley [Public.Conversation]
-getRemoteConversations zusr remoteConvs = do
- localDomain <- viewFederationDomain
- let qualifiedZUser = Qualified zusr localDomain
- let convsByDomain = partitionRemote remoteConvs
- convs <- pooledForConcurrentlyN 8 convsByDomain $ \(remoteDomain, convIds) -> do
- let req = FederatedGalley.GetConversationsRequest qualifiedZUser convIds
- rpc = FederatedGalley.getConversations FederatedGalley.clientRoutes req
- gcresConvs <$> runFederatedGalley remoteDomain rpc
- pure $ concat convs
-
-getRemoteConversationsWithFailures :: UserId -> [Remote ConvId] -> Galley ([Qualified ConvId], [Public.Conversation])
-getRemoteConversationsWithFailures zusr remoteConvs = do
+getRemoteConversations zusr remoteConvs =
+ getRemoteConversationsWithFailures zusr remoteConvs >>= \case
+ -- throw first error
+ (failed : _, _) -> throwM (fgcError failed)
+ ([], result) -> pure result
+
+data FailedGetConversationReason
+ = FailedGetConversationLocally
+ | FailedGetConversationRemotely FederationError
+
+fgcrError :: FailedGetConversationReason -> Wai.Error
+fgcrError FailedGetConversationLocally = errorDescriptionToWai convNotFound
+fgcrError (FailedGetConversationRemotely e) = federationErrorToWai e
+
+data FailedGetConversation
+ = FailedGetConversation
+ [Qualified ConvId]
+ FailedGetConversationReason
+
+fgcError :: FailedGetConversation -> Wai.Error
+fgcError (FailedGetConversation _ r) = fgcrError r
+
+failedGetConversationRemotely ::
+ [Qualified ConvId] -> FederationError -> FailedGetConversation
+failedGetConversationRemotely qconvs =
+ FailedGetConversation qconvs . FailedGetConversationRemotely
+
+failedGetConversationLocally ::
+ [Qualified ConvId] -> FailedGetConversation
+failedGetConversationLocally qconvs =
+ FailedGetConversation qconvs FailedGetConversationLocally
+
+partitionGetConversationFailures ::
+ [FailedGetConversation] -> ([Qualified ConvId], [Qualified ConvId])
+partitionGetConversationFailures = bimap concat concat . partitionEithers . map split
+ where
+ split (FailedGetConversation convs FailedGetConversationLocally) = Left convs
+ split (FailedGetConversation convs (FailedGetConversationRemotely _)) = Right convs
+
+getRemoteConversationsWithFailures ::
+ UserId ->
+ [Remote ConvId] ->
+ Galley ([FailedGetConversation], [Public.Conversation])
+getRemoteConversationsWithFailures zusr convs = do
localDomain <- viewFederationDomain
- let qualifiedZUser = Qualified zusr localDomain
- let convsByDomain = partitionRemote remoteConvs
- convs <- pooledForConcurrentlyN 8 convsByDomain $ \(remoteDomain, convIds) -> handleFailures remoteDomain convIds $ do
- let req = FederatedGalley.GetConversationsRequest qualifiedZUser convIds
- rpc = FederatedGalley.getConversations FederatedGalley.clientRoutes req
- gcresConvs <$> executeFederated remoteDomain rpc
- pure $ concatEithers convs
+
+ -- get self member statuses from the database
+ statusMap <- Data.remoteConversationStatus zusr convs
+ let remoteView rconv =
+ Mapping.remoteConversationView
+ zusr
+ ( Map.findWithDefault
+ defMemberStatus
+ (toRemote (cnvmQualifiedId (FederatedGalley.rcnvMetadata rconv)))
+ statusMap
+ )
+ rconv
+ (locallyFound, locallyNotFound) = partition (flip Map.member statusMap) convs
+ localFailures
+ | null locallyNotFound = []
+ | otherwise = [failedGetConversationLocally (map unTagged locallyNotFound)]
+
+ -- request conversations from remote backends
+ fmap (bimap (localFailures <>) concat . partitionEithers)
+ . pooledForConcurrentlyN 8 (partitionRemote locallyFound)
+ $ \(domain, someConvs) -> do
+ let req = FederatedGalley.GetConversationsRequest zusr someConvs
+ rpc = FederatedGalley.getConversations FederatedGalley.clientRoutes localDomain req
+ handleFailures (map (flip Qualified domain) someConvs) $ do
+ rconvs <- gcresConvs <$> executeFederated domain rpc
+ pure $ catMaybes (map remoteView rconvs)
where
- handleFailures :: Domain -> [ConvId] -> ExceptT FederationError Galley a -> Galley (Either [Qualified ConvId] a)
- handleFailures domain convIds action = do
- res <- runExceptT action
- case res of
- Right a -> pure $ Right a
- Left e -> do
- Logger.warn $
- Logger.msg ("Error occurred while fetching remote conversations" :: ByteString)
- . Logger.field "error" (show e)
- pure . Left $ map (`Qualified` domain) convIds
- concatEithers :: (Monoid a, Monoid b) => [Either a b] -> (a, b)
- concatEithers = bimap mconcat mconcat . partitionEithers
+ handleFailures ::
+ [Qualified ConvId] ->
+ ExceptT FederationError Galley a ->
+ Galley (Either FailedGetConversation a)
+ handleFailures qconvs action = runExceptT
+ . withExceptT (failedGetConversationRemotely qconvs)
+ . catchE action
+ $ \e -> do
+ lift . Logger.warn $
+ Logger.msg ("Error occurred while fetching remote conversations" :: ByteString)
+ . Logger.field "error" (show e)
+ throwE e
getConversationRoles :: UserId -> ConvId -> Galley Public.ConversationRolesList
getConversationRoles zusr cnv = do
@@ -292,7 +343,6 @@ listConversationsV2 user (Public.ListConversationsV2 ids) = do
let (remoteIds, localIds) = partitionRemoteOrLocalIds' localDomain (fromRange ids)
(foundLocalIds, notFoundLocalIds) <- foundsAndNotFounds (Data.localConversationIdsOf user) localIds
- (foundRemoteIds, locallyNotFoundRemoteIds) <- foundsAndNotFounds (Data.remoteConversationIdOf user) remoteIds
localInternalConversations <-
Data.conversations foundLocalIds
@@ -300,9 +350,11 @@ listConversationsV2 user (Public.ListConversationsV2 ids) = do
>>= filterM (pure . isMember user . Data.convLocalMembers)
localConversations <- mapM (Mapping.conversationView user) localInternalConversations
- (remoteFailures, remoteConversations) <- getRemoteConversationsWithFailures user foundRemoteIds
- let fetchedOrFailedRemoteIds = Set.fromList $ map Public.cnvQualifiedId remoteConversations <> remoteFailures
- remoteNotFoundRemoteIds = filter (`Set.notMember` fetchedOrFailedRemoteIds) $ map unTagged foundRemoteIds
+ (remoteFailures, remoteConversations) <- getRemoteConversationsWithFailures user remoteIds
+ let (failedConvsLocally, failedConvsRemotely) = partitionGetConversationFailures remoteFailures
+ failedConvs = failedConvsLocally <> failedConvsRemotely
+ fetchedOrFailedRemoteIds = Set.fromList $ map Public.cnvQualifiedId remoteConversations <> failedConvs
+ remoteNotFoundRemoteIds = filter (`Set.notMember` fetchedOrFailedRemoteIds) $ map unTagged remoteIds
unless (null remoteNotFoundRemoteIds) $
-- FUTUREWORK: This implies that the backends are out of sync. Maybe the
-- current user should be considered removed from this conversation at this
@@ -316,10 +368,10 @@ listConversationsV2 user (Public.ListConversationsV2 ids) = do
Public.ConversationsResponse
{ crFound = allConvs,
crNotFound =
- map unTagged locallyNotFoundRemoteIds
+ failedConvsLocally
<> remoteNotFoundRemoteIds
<> map (`Qualified` localDomain) notFoundLocalIds,
- crFailed = remoteFailures
+ crFailed = failedConvsRemotely
}
where
removeDeleted :: Data.Conversation -> Galley Bool
@@ -355,7 +407,7 @@ getLocalSelf :: UserId -> ConvId -> Galley (Maybe Public.Member)
getLocalSelf usr cnv = do
alive <- Data.isConvAlive cnv
if alive
- then Mapping.toMember <$$> Data.member cnv usr
+ then Mapping.localMemberToSelf <$$> Data.member cnv usr
else Nothing <$ Data.deleteConversation cnv
getConversationMetaH :: ConvId -> Galley Response
@@ -364,11 +416,12 @@ getConversationMetaH cnv = do
Nothing -> setStatus status404 empty
Just meta -> json meta
-getConversationMeta :: ConvId -> Galley (Maybe ConversationMeta)
+getConversationMeta :: ConvId -> Galley (Maybe ConversationMetadata)
getConversationMeta cnv = do
alive <- Data.isConvAlive cnv
+ localDomain <- viewFederationDomain
if alive
- then Data.conversationMeta cnv
+ then Data.conversationMeta localDomain cnv
else do
Data.deleteConversation cnv
pure Nothing
diff --git a/services/galley/src/Galley/API/Teams.hs b/services/galley/src/Galley/API/Teams.hs
index b2f8e53e38..49e6ee477b 100644
--- a/services/galley/src/Galley/API/Teams.hs
+++ b/services/galley/src/Galley/API/Teams.hs
@@ -742,7 +742,7 @@ uncheckedDeleteTeamMember zusr zcon tid remove mems = do
let qconvId = Qualified (Data.convId dc) localDomain
qusr = Qualified zusr localDomain
let (bots, users) = localBotsAndUsers (Data.convLocalMembers dc)
- let x = filter (\m -> not (Conv.memId m `Set.member` exceptTo)) users
+ let x = filter (\m -> not (Conv.lmId m `Set.member` exceptTo)) users
let y = Conv.Event Conv.MemberLeave qconvId qusr now edata
for_ (newPushLocal (mems ^. teamMemberListType) zusr (ConvEvent y) (recipient <$> x)) $ \p ->
push1 $ p & pushConn .~ zcon
diff --git a/services/galley/src/Galley/API/Update.hs b/services/galley/src/Galley/API/Update.hs
index 7d6b656f7e..0d2ea6c72e 100644
--- a/services/galley/src/Galley/API/Update.hs
+++ b/services/galley/src/Galley/API/Update.hs
@@ -36,7 +36,7 @@ module Galley.API.Update
-- * Managing Members
addMembersH,
addMembers,
- updateLocalSelfMember,
+ updateUnqualifiedSelfMember,
updateSelfMember,
updateOtherMemberH,
removeMember,
@@ -99,7 +99,6 @@ import Galley.Types
import Galley.Types.Bot hiding (addBot)
import Galley.Types.Clients (Clients)
import qualified Galley.Types.Clients as Clients
-import Galley.Types.Conversations.Members (RemoteMember (rmConvRoleName, rmId))
import Galley.Types.Conversations.Roles (Action (..), RoleName, roleNameWireMember)
import Galley.Types.Teams hiding (Event, EventData (..), EventType (..), self)
import Galley.Validation
@@ -255,16 +254,16 @@ uncheckedUpdateConversationAccess body usr zcon conv (currentAccess, targetAcces
-- to make assumption about the order of roles and implement policy
-- based on those assumptions.
when (currentRole > ActivatedAccessRole && targetRole <= ActivatedAccessRole) $ do
- mIds <- map memId <$> use usersL
+ mIds <- map lmId <$> use usersL
activated <- fmap User.userId <$> lift (lookupActivatedUsers mIds)
- let isActivated user = memId user `elem` activated
+ let isActivated user = lmId user `elem` activated
usersL %= filter isActivated
-- In a team-only conversation we also want to remove bots and guests
case (targetRole, Data.convTeam conv) of
(TeamAccessRole, Just tid) -> do
currentUsers <- use usersL
onlyTeamUsers <- flip filterM currentUsers $ \user ->
- lift $ isJust <$> Data.teamMember tid (memId user)
+ lift $ isJust <$> Data.teamMember tid (lmId user)
assign usersL onlyTeamUsers
botsL .= []
_ -> return ()
@@ -272,9 +271,9 @@ uncheckedUpdateConversationAccess body usr zcon conv (currentAccess, targetAcces
now <- liftIO getCurrentTime
let accessEvent = Event ConvAccessUpdate qcnv qusr now (EdConvAccessUpdate body)
Data.updateConversationAccess cnv targetAccess targetRole
- pushConversationEvent (Just zcon) accessEvent (map memId users) bots
+ pushConversationEvent (Just zcon) accessEvent (map lmId users) bots
-- Remove users and bots
- let removedUsers = map memId users \\ map memId newUsers
+ let removedUsers = map lmId users \\ map lmId newUsers
removedBots = map botMemId bots \\ map botMemId newBots
mapM_ (deleteBot cnv) removedBots
case removedUsers of
@@ -316,7 +315,7 @@ updateConversationReceiptMode usr zcon cnv receiptModeUpdate@(Public.Conversatio
Data.updateConversationReceiptMode cnv target
now <- liftIO getCurrentTime
let receiptEvent = Event ConvReceiptModeUpdate qcnv qusr now (EdConvReceiptModeUpdate receiptModeUpdate)
- pushConversationEvent (Just zcon) receiptEvent (map memId users) bots
+ pushConversationEvent (Just zcon) receiptEvent (map lmId users) bots
pure receiptEvent
updateConversationMessageTimerH :: UserId ::: ConnId ::: ConvId ::: JsonRequest Public.ConversationMessageTimerUpdate -> Galley Response
@@ -345,7 +344,7 @@ updateConversationMessageTimer usr zcon cnv timerUpdate@(Public.ConversationMess
now <- liftIO getCurrentTime
let timerEvent = Event ConvMessageTimerUpdate qcnv qusr now (EdConvMessageTimerUpdate timerUpdate)
Data.updateConversationMessageTimer cnv target
- pushConversationEvent (Just zcon) timerEvent (map memId users) bots
+ pushConversationEvent (Just zcon) timerEvent (map lmId users) bots
pure timerEvent
addCodeH :: UserId ::: ConnId ::: ConvId -> Galley Response
@@ -376,7 +375,7 @@ addCode usr zcon cnv = do
now <- liftIO getCurrentTime
conversationCode <- createCode code
let event = Event ConvCodeUpdate qcnv qusr now (EdConvCodeUpdate conversationCode)
- pushConversationEvent (Just zcon) event (map memId users) bots
+ pushConversationEvent (Just zcon) event (map lmId users) bots
pure $ CodeAdded event
Just code -> do
conversationCode <- createCode code
@@ -404,7 +403,7 @@ rmCode usr zcon cnv = do
Data.deleteCode key ReusableCode
now <- liftIO getCurrentTime
let event = Event ConvCodeDelete qcnv qusr now EdConvCodeDelete
- pushConversationEvent (Just zcon) event (map memId users) bots
+ pushConversationEvent (Just zcon) event (map lmId users) bots
pure event
getCodeH :: UserId ::: ConvId -> Galley Response
@@ -494,7 +493,7 @@ addMembers zusr zcon convId invite = do
checkRemoteUsersExist newRemotes
checkLHPolicyConflictsLocal conv newLocals
checkLHPolicyConflictsRemote (FutureWork newRemotes)
- addToConversation mems rMems (zusr, memConvRoleName self) zcon (withRoles newLocals) (withRoles newRemotes) conv
+ addToConversation mems rMems (zusr, lmConvRoleName self) zcon (withRoles newLocals) (withRoles newRemotes) conv
where
userIsMember u = (^. userId . to (== u))
@@ -520,7 +519,7 @@ addMembers zusr zcon convId invite = do
allNewUsersGaveConsent <- allLegalholdConsentGiven newUsers
- whenM (anyLegalholdActivated (memId <$> convUsers)) $
+ whenM (anyLegalholdActivated (lmId <$> convUsers)) $
unless allNewUsersGaveConsent $
throwErrorDescription missingLegalholdConsent
@@ -529,12 +528,12 @@ addMembers zusr zcon convId invite = do
throwErrorDescription missingLegalholdConsent
convUsersLHStatus <- do
- uidsStatus <- getLHStatusForUsers (memId <$> convUsers)
+ uidsStatus <- getLHStatusForUsers (lmId <$> convUsers)
pure $ zipWith (\mem (_, status) -> (mem, status)) convUsers uidsStatus
if any
( \(mem, status) ->
- memConvRoleName mem == roleNameWireAdmin
+ lmConvRoleName mem == roleNameWireAdmin
&& consentGiven status == ConsentGiven
)
convUsersLHStatus
@@ -542,9 +541,9 @@ addMembers zusr zcon convId invite = do
localDomain <- viewFederationDomain
for_ convUsersLHStatus $ \(mem, status) ->
when (consentGiven status == ConsentNotGiven) $
- let qvictim = Qualified (memId mem) localDomain
+ let qvictim = Qualified (lmId mem) localDomain
in void $
- removeMember (memId mem `Qualified` localDomain) Nothing (Data.convId conv `Qualified` localDomain) qvictim
+ removeMember (lmId mem `Qualified` localDomain) Nothing (Data.convId conv `Qualified` localDomain) qvictim
else throwErrorDescription missingLegalholdConsent
checkLHPolicyConflictsRemote :: FutureWork 'LegalholdPlusFederationNotImplemented [Remote UserId] -> Galley ()
@@ -554,14 +553,36 @@ updateSelfMember :: UserId -> ConnId -> Qualified ConvId -> Public.MemberUpdate
updateSelfMember zusr zcon qcnv update = do
localDomain <- viewFederationDomain
if qDomain qcnv == localDomain
- then updateLocalSelfMember zusr zcon (qUnqualified qcnv) update
- else throwM federationNotImplemented
+ then updateLocalSelfMember zusr zcon (toLocal qcnv) update
+ else updateRemoteSelfMember zusr zcon (toRemote qcnv) update
-updateLocalSelfMember :: UserId -> ConnId -> ConvId -> Public.MemberUpdate -> Galley ()
-updateLocalSelfMember zusr zcon cid update = do
- conv <- getConversationAndCheckMembership zusr cid
+updateUnqualifiedSelfMember :: UserId -> ConnId -> ConvId -> Public.MemberUpdate -> Galley ()
+updateUnqualifiedSelfMember zusr zcon cid update = do
+ localDomain <- viewFederationDomain
+ updateLocalSelfMember zusr zcon (toLocal (Qualified cid localDomain)) update
+
+updateLocalSelfMember :: UserId -> ConnId -> Local ConvId -> Public.MemberUpdate -> Galley ()
+updateLocalSelfMember zusr zcon (Tagged qcid) update = do
+ -- FUTUREWORK: no need to fetch the whole conversation here: the
+ -- getConversationAndCheckMembership function results in 3 queries (for the
+ -- conversation metadata, remote members and local members respectively), but
+ -- only one is really needed (local members).
+ conv <- getConversationAndCheckMembership zusr (qUnqualified qcid)
m <- getSelfMemberFromLocalsLegacy zusr (Data.convLocalMembers conv)
- void $ processUpdateMemberEvent zusr zcon cid [m] m update
+ void $ processUpdateMemberEvent zusr zcon qcid [lmId m] (lmId m) update
+
+updateRemoteSelfMember ::
+ UserId ->
+ ConnId ->
+ Remote ConvId ->
+ Public.MemberUpdate ->
+ Galley ()
+updateRemoteSelfMember zusr zcon rcid update = do
+ statusMap <- Data.remoteConversationStatus zusr [rcid]
+ case Map.lookup rcid statusMap of
+ Nothing -> throwM convMemberNotFound
+ Just _ ->
+ void $ processUpdateMemberEvent zusr zcon (unTagged rcid) [zusr] zusr update
updateOtherMemberH :: UserId ::: ConnId ::: ConvId ::: UserId ::: JsonRequest Public.OtherMemberUpdate -> Galley Response
updateOtherMemberH (zusr ::: zcon ::: cid ::: victim ::: req) = do
@@ -571,13 +592,15 @@ updateOtherMemberH (zusr ::: zcon ::: cid ::: victim ::: req) = do
updateOtherMember :: UserId -> ConnId -> ConvId -> UserId -> Public.OtherMemberUpdate -> Galley ()
updateOtherMember zusr zcon cid victim update = do
+ localDomain <- viewFederationDomain
when (zusr == victim) $
throwM invalidTargetUserOp
conv <- getConversationAndCheckMembership zusr cid
let (bots, users) = localBotsAndUsers (Data.convLocalMembers conv)
ensureActionAllowedThrowing ModifyOtherConversationMember =<< getSelfMemberFromLocalsLegacy zusr users
+ -- this has the side effect of checking that the victim is indeed part of the conversation
memTarget <- getOtherMemberLegacy victim users
- e <- processUpdateMemberEvent zusr zcon cid users memTarget update
+ e <- processUpdateMemberEvent zusr zcon (Qualified cid localDomain) (map lmId users) (lmId memTarget) update
void . forkIO $ void $ External.deliver (bots `zip` repeat e)
-- | A general conversation member removal function used both by the unqualified
@@ -647,7 +670,7 @@ removeMemberFromLocalConv remover@(Qualified removerUid removerDomain) zcon conv
removerRole <-
withExceptT (const @_ @ConvNotFound RemoveFromConversationErrorNotFound) $
if localDomain == removerDomain
- then memConvRoleName <$> getSelfMemberFromLocals removerUid locals
+ then lmConvRoleName <$> getSelfMemberFromLocals removerUid locals
else rmConvRoleName <$> getSelfMemberFromRemotes (toRemote remover) (Data.convRemoteMembers conv)
generalConvChecks localDomain removerRole conv
@@ -838,7 +861,7 @@ newMessage qusr con qcnv msg now (m, c, t) ~(toBots, toUsers) =
-- use recipient's client's self conversation on broadcast
-- (with federation, this might not work for remote members)
-- FUTUREWORK: for remote recipients, set the domain correctly here
- qconv = fromMaybe ((`Qualified` qDomain qusr) . selfConv $ memId m) qcnv
+ qconv = fromMaybe ((`Qualified` qDomain qusr) . selfConv $ lmId m) qcnv
e = Event OtrMessageAdd qconv qusr now (EdOtrMessage o)
r = recipient m & recipientClients .~ RecipientClientsSome (singleton c)
in case newBotMember m of
@@ -1024,7 +1047,7 @@ addToConversation (bots, existingLocals) existingRemotes (usr, usrRole) conn new
localDomain <- viewFederationDomain
(e, lmm, rmm) <- Data.addMembersWithRole localDomain now (Data.convId c) (usr, usrRole) mems
let newMembersWithRoles =
- ((flip Qualified localDomain . memId &&& memConvRoleName) <$> lmm)
+ ((flip Qualified localDomain . lmId &&& lmConvRoleName) <$> lmm)
<> ((unTagged . rmId &&& rmConvRoleName) <$> rmm)
case newMembersWithRoles of
[] ->
@@ -1033,7 +1056,7 @@ addToConversation (bots, existingLocals) existingRemotes (usr, usrRole) conn new
let action = FederatedGalley.ConversationMembersActionAdd (x :| xs)
qusr = Qualified usr localDomain
notifyRemoteAboutConvUpdate qusr (convId c) now action (rmId <$> existingRemotes <> rmm)
- let localsToNotify = nubOrd . fmap memId $ existingLocals <> lmm
+ let localsToNotify = nubOrd . fmap lmId $ existingLocals <> lmm
pushConversationEvent (Just conn) e localsToNotify bots
pure $ Updated e
@@ -1074,23 +1097,34 @@ ensureConvMember users usr =
unless (usr `isMember` users) $
throwErrorDescription convNotFound
+-- | Update a member of a conversation and propagate events.
+--
+-- Note: the target is assumed to be a member of the conversation.
processUpdateMemberEvent ::
Data.IsMemberUpdate mu =>
+ -- | Originating user
UserId ->
+ -- | Connection ID for the originating user
ConnId ->
- ConvId ->
- [LocalMember] ->
- LocalMember ->
+ -- | Conversation whose members are being updated
+ Qualified ConvId ->
+ -- | Recipients of the notification
+ [UserId] ->
+ -- | User being updated
+ UserId ->
+ -- | Update structure
mu ->
Galley Event
-processUpdateMemberEvent zusr zcon cid users target update = do
+processUpdateMemberEvent zusr zcon qcid users target update = do
localDomain <- viewFederationDomain
- let qcnv = Qualified cid localDomain
- qusr = Qualified zusr localDomain
- up <- Data.updateMember cid (memId target) update
+ let qusr = Qualified zusr localDomain
+ up <-
+ if localDomain == qDomain qcid
+ then Data.updateMember (qUnqualified qcid) target update
+ else Data.updateMemberRemoteConv (toRemote qcid) target update
now <- liftIO getCurrentTime
- let e = Event MemberStateUpdate qcnv qusr now (EdMemberUpdate up)
- let recipients = fmap recipient (target : filter ((/= memId target) . memId) users)
+ let e = Event MemberStateUpdate qcid qusr now (EdMemberUpdate up)
+ let recipients = fmap userRecipient (target : filter (/= target) users)
for_ (newPushLocal ListComplete zusr (ConvEvent e) recipients) $ \p ->
push1 $
p
@@ -1175,7 +1209,7 @@ withValidOtrRecipients utype usr clt cnv rcps val now go = do
pure $ OtrConversationNotFound convNotFound
else do
localMembers <- Data.members cnv
- let localMemberIds = memId <$> localMembers
+ let localMemberIds = lmId <$> localMembers
isInternal <- view $ options . optSettings . setIntraListing
clts <-
if isInternal
@@ -1254,8 +1288,8 @@ checkOtrRecipients usr sid prs vms vcs val now
| otherwise = Nothing
-- Valid recipient members & clients
- vmembers :: Map UserId (InternalMember UserId)
- vmembers = Map.fromList $ map (\m -> (memId m, m)) vms
+ vmembers :: Map UserId LocalMember
+ vmembers = Map.fromList $ map (\m -> (lmId m, m)) vms
vclients :: Clients
vclients = Clients.rmClient usr sid vcs
diff --git a/services/galley/src/Galley/API/Util.hs b/services/galley/src/Galley/API/Util.hs
index 98020dcfc9..e7bbd6d43a 100644
--- a/services/galley/src/Galley/API/Util.hs
+++ b/services/galley/src/Galley/API/Util.hs
@@ -50,8 +50,7 @@ import Galley.Intra.Push
import Galley.Intra.User
import Galley.Options (optSettings, setFeatureFlags, setFederationDomain)
import Galley.Types
-import Galley.Types.Conversations.Members (RemoteMember (..))
-import qualified Galley.Types.Conversations.Members as Members
+import Galley.Types.Conversations.Members (localMemberToOther, remoteMemberToOther)
import Galley.Types.Conversations.Roles
import Galley.Types.Teams hiding (Event)
import Imports
@@ -143,9 +142,9 @@ ensureActionAllowed action role = case isActionAllowed action role of
-- is permitted.
-- If not, throw 'Member'; if the user is found and does not have the given permission, throw
-- 'operationDenied'. Otherwise, return the found user.
-ensureActionAllowedThrowing :: Action -> InternalMember a -> Galley ()
+ensureActionAllowedThrowing :: Action -> LocalMember -> Galley ()
ensureActionAllowedThrowing action mem =
- case ensureActionAllowed action (memConvRoleName mem) of
+ case ensureActionAllowed action (lmConvRoleName mem) of
ACOAllowed -> return ()
ACOActionDenied _ -> throwErrorDescription (actionDenied action)
ACOCustomRolesNotSupported -> throwM (badRequest "Custom roles not supported")
@@ -157,9 +156,9 @@ ensureActionAllowedThrowing action mem =
-- own. This is used to ensure users cannot "elevate" allowed actions
-- This function needs to be review when custom roles are introduced since only
-- custom roles can cause `roleNameToActions` to return a Nothing
-ensureConvRoleNotElevated :: InternalMember a -> RoleName -> Galley ()
+ensureConvRoleNotElevated :: LocalMember -> RoleName -> Galley ()
ensureConvRoleNotElevated origMember targetRole = do
- case (roleNameToActions targetRole, roleNameToActions (memConvRoleName origMember)) of
+ case (roleNameToActions targetRole, roleNameToActions (lmConvRoleName origMember)) of
(Just targetActions, Just memberActions) ->
unless (Set.isSubsetOf targetActions memberActions) $
throwM invalidActions
@@ -220,7 +219,7 @@ acceptOne2One usr conv conn = do
throwM badConvState
now <- liftIO getCurrentTime
(e, mm) <- Data.addMember localDomain now cid usr
- conv' <- if isJust (find ((usr /=) . memId) mems) then promote else pure conv
+ conv' <- if isJust (find ((usr /=) . lmId) mems) then promote else pure conv
let mems' = mems <> toList mm
for_ (newPushLocal ListComplete usr (ConvEvent e) (recipient <$> mems')) $ \p ->
push1 $ p & pushConn .~ conn & pushRoute .~ RouteDirect
@@ -237,22 +236,19 @@ acceptOne2One usr conv conn = do
"Connect conversation with more than 2 members: "
<> LT.pack (show cid)
-isBot :: InternalMember a -> Bool
-isBot = isJust . memService
+isBot :: LocalMember -> Bool
+isBot = isJust . lmService
-isMember :: (Eq a, Foldable m) => a -> m (InternalMember a) -> Bool
-isMember u = isJust . find ((u ==) . memId)
+isMember :: Foldable m => UserId -> m LocalMember -> Bool
+isMember u = isJust . find ((u ==) . lmId)
-isRemoteMember :: (Foldable m) => Remote UserId -> m RemoteMember -> Bool
+isRemoteMember :: Foldable m => Remote UserId -> m RemoteMember -> Bool
isRemoteMember u = isJust . find ((u ==) . rmId)
-findMember :: Data.Conversation -> UserId -> Maybe LocalMember
-findMember c u = find ((u ==) . memId) (Data.convLocalMembers c)
-
localBotsAndUsers :: Foldable f => f LocalMember -> ([BotMember], [LocalMember])
localBotsAndUsers = foldMap botOrUser
where
- botOrUser m = case memService m of
+ botOrUser m = case lmService m of
-- we drop invalid bots here, which shouldn't happen
Just _ -> (toList (newBotMember m), [])
Nothing -> ([], [m])
@@ -261,7 +257,7 @@ location :: ToByteString a => a -> Response -> Response
location = addHeader hLocation . toByteString'
nonTeamMembers :: [LocalMember] -> [TeamMember] -> [LocalMember]
-nonTeamMembers cm tm = filter (not . isMemberOfTeam . memId) cm
+nonTeamMembers cm tm = filter (not . isMemberOfTeam . lmId) cm
where
-- FUTUREWORK: remote members: teams and their members are always on the same backend
isMemberOfTeam = \case
@@ -269,7 +265,7 @@ nonTeamMembers cm tm = filter (not . isMemberOfTeam . memId) cm
convMembsAndTeamMembs :: [LocalMember] -> [TeamMember] -> [Recipient]
convMembsAndTeamMembs convMembs teamMembs =
- fmap userRecipient . setnub $ map memId convMembs <> map (view userId) teamMembs
+ fmap userRecipient . setnub $ map lmId convMembs <> map (view userId) teamMembs
where
setnub = Set.toList . Set.fromList
@@ -339,7 +335,7 @@ getLocalMember ::
UserId ->
t LocalMember ->
ExceptT e m LocalMember
-getLocalMember = getMember memId
+getLocalMember = getMember lmId
-- | Since we search by remote user ID, we know that the member must be remote.
getRemoteMember ::
@@ -498,21 +494,9 @@ toNewRemoteConversation now localDomain Data.Conversation {..} =
[RemoteMember] ->
Set OtherMember
toMembers ls rs =
- Set.fromList $ fmap localToOther ls <> fmap remoteToOther rs
- localToOther :: LocalMember -> OtherMember
- localToOther Members.InternalMember {..} =
- OtherMember
- { omQualifiedId = Qualified memId localDomain,
- omService = Nothing,
- omConvRoleName = memConvRoleName
- }
- remoteToOther :: RemoteMember -> OtherMember
- remoteToOther RemoteMember {..} =
- OtherMember
- { omQualifiedId = unTagged rmId,
- omService = Nothing,
- omConvRoleName = rmConvRoleName
- }
+ Set.fromList $
+ map (localMemberToOther localDomain) ls
+ <> map remoteMemberToOther rs
-- | The function converts a 'NewRemoteConversation' value to a
-- 'Wire.API.Conversation.Conversation' value for each user that is on the given
@@ -553,21 +537,22 @@ fromNewRemoteConversation d NewRemoteConversation {..} =
conv :: Public.Member -> [OtherMember] -> Public.Conversation
conv this others =
Public.Conversation
- { cnvQualifiedId = rcCnvId,
- cnvType = rcCnvType,
- -- FUTUREWORK: Document this is the same domain as the conversation
- -- domain
- cnvCreator = qUnqualified rcOrigUserId,
- cnvAccess = rcCnvAccess,
- cnvAccessRole = rcCnvAccessRole,
- cnvName = rcCnvName,
- cnvMembers = ConvMembers this others,
- -- FUTUREWORK: Document this is the same domain as the conversation
- -- domain.
- cnvTeam = Nothing,
- cnvMessageTimer = rcMessageTimer,
- cnvReceiptMode = rcReceiptMode
- }
+ ConversationMetadata
+ { cnvmQualifiedId = rcCnvId,
+ cnvmType = rcCnvType,
+ -- FUTUREWORK: Document this is the same domain as the conversation
+ -- domain
+ cnvmCreator = qUnqualified rcOrigUserId,
+ cnvmAccess = rcCnvAccess,
+ cnvmAccessRole = rcCnvAccessRole,
+ cnvmName = rcCnvName,
+ -- FUTUREWORK: Document this is the same domain as the conversation
+ -- domain.
+ cnvmTeam = Nothing,
+ cnvmMessageTimer = rcMessageTimer,
+ cnvmReceiptMode = rcReceiptMode
+ }
+ (ConvMembers this others)
-- | Notify remote users of being added to a new conversation
registerRemoteConversationMemberships ::
diff --git a/services/galley/src/Galley/Data.hs b/services/galley/src/Galley/Data.hs
index ef4d8f8a4b..3592ab08e6 100644
--- a/services/galley/src/Galley/Data.hs
+++ b/services/galley/src/Galley/Data.hs
@@ -55,15 +55,16 @@ module Galley.Data
-- * Conversations
Conversation (..),
+ convMetadata,
acceptConnect,
conversation,
conversationIdsFrom,
localConversationIdsOf,
- remoteConversationIdOf,
+ remoteConversationStatus,
localConversationIdsPageFrom,
conversationIdRowsForPagination,
- conversationMeta,
conversations,
+ conversationMeta,
conversationsRemote,
createConnectConversation,
createConversation,
@@ -191,7 +192,7 @@ mkResultSet page = ResultSet (result page) typ
| otherwise = ResultSetComplete
schemaVersion :: Int32
-schemaVersion = 52
+schemaVersion = 53
-- | Insert a conversation code
insertCode :: MonadClient m => Code -> m ()
@@ -535,12 +536,22 @@ toConv cid mms remoteMems conv =
where
f ms (cty, uid, acc, role, nme, ti, del, timer, rm) = Conversation cid cty uid nme (defAccess cty acc) (maybeRole cty role) ms remoteMems ti del timer rm
-conversationMeta :: MonadClient m => ConvId -> m (Maybe ConversationMeta)
-conversationMeta conv =
+conversationMeta :: MonadClient m => Domain -> ConvId -> m (Maybe ConversationMetadata)
+conversationMeta localDomain conv =
fmap toConvMeta
<$> retry x1 (query1 Cql.selectConv (params Quorum (Identity conv)))
where
- toConvMeta (t, c, a, r, n, i, _, mt, rm) = ConversationMeta conv t c (defAccess t a) (maybeRole t r) n i mt rm
+ toConvMeta (t, c, a, r, n, i, _, mt, rm) =
+ ConversationMetadata
+ (Qualified conv localDomain)
+ t
+ c
+ (defAccess t a)
+ (maybeRole t r)
+ n
+ i
+ mt
+ rm
-- | Deprecated, use 'localConversationIdsPageFrom'
conversationIdsFrom ::
@@ -582,16 +593,27 @@ localConversationIdsOf :: forall m. (MonadClient m, MonadUnliftIO m) => UserId -
localConversationIdsOf usr cids = do
runIdentity <$$> retry x1 (query Cql.selectUserConvsIn (params Quorum (usr, cids)))
--- | Takes a list of remote conversation ids and splits them by those found for
--- the given user
-remoteConversationIdOf :: forall m. (MonadClient m, MonadLogger m, MonadUnliftIO m) => UserId -> [Remote ConvId] -> m [Remote ConvId]
-remoteConversationIdOf usr cnvs = do
- concat <$$> pooledMapConcurrentlyN 8 findRemoteConvs . Map.assocs . partitionQualified . map unTagged $ cnvs
+-- | Takes a list of remote conversation ids and fetches member status flags
+-- for the given user
+remoteConversationStatus ::
+ (MonadClient m, MonadUnliftIO m) =>
+ UserId ->
+ [Remote ConvId] ->
+ m (Map (Remote ConvId) MemberStatus)
+remoteConversationStatus uid =
+ fmap mconcat
+ . pooledMapConcurrentlyN 8 (uncurry (remoteConversationStatusOnDomain uid))
+ . partitionRemote
+
+remoteConversationStatusOnDomain :: MonadClient m => UserId -> Domain -> [ConvId] -> m (Map (Remote ConvId) MemberStatus)
+remoteConversationStatusOnDomain uid domain convs =
+ Map.fromList . map toPair
+ <$> query Cql.selectRemoteConvMembers (params Quorum (uid, domain, convs))
where
- findRemoteConvs :: (Domain, [ConvId]) -> m [Remote ConvId]
- findRemoteConvs (domain, remoteConvIds) = do
- foundCnvs <- runIdentity <$$> query Cql.selectRemoteConvMembershipIn (params Quorum (usr, domain, remoteConvIds))
- pure $ toRemote . (`Qualified` domain) <$> foundCnvs
+ toPair (conv, omus, omur, oar, oarr, hid, hidr) =
+ ( toRemote (Qualified conv domain),
+ toMemberStatus (omus, omur, oar, oarr, hid, hidr)
+ )
conversationsRemote :: (MonadClient m) => UserId -> m [Remote ConvId]
conversationsRemote usr = do
@@ -701,7 +723,7 @@ deleteConversation :: (MonadClient m, Log.MonadLogger m, MonadThrow m) => ConvId
deleteConversation cid = do
retry x5 $ write Cql.markConvDeleted (params Quorum (Identity cid))
mm <- members cid
- for_ mm $ \m -> removeMember (memId m) cid
+ for_ mm $ \m -> removeMember (lmId m) cid
retry x5 $ write Cql.deleteConv (params Quorum (Identity cid))
acceptConnect :: MonadClient m => ConvId -> m ()
@@ -743,6 +765,19 @@ newConv cid ct usr mems rMems acc role name tid mtimer rMode =
convReceiptMode = rMode
}
+convMetadata :: Domain -> Conversation -> ConversationMetadata
+convMetadata localDomain c =
+ ConversationMetadata
+ (Qualified (convId c) localDomain)
+ (convType c)
+ (convCreator c)
+ (convAccess c)
+ (convAccessRole c)
+ (convName c)
+ (convTeam c)
+ (convMessageTimer c)
+ (convReceiptMode c)
+
defAccess :: ConvType -> Maybe (Set Access) -> [Access]
defAccess SelfConv Nothing = [PrivateAccess]
defAccess ConnectConv Nothing = [PrivateAccess]
@@ -781,8 +816,8 @@ member ::
UserId ->
m (Maybe LocalMember)
member cnv usr =
- fmap (join @Maybe) . traverse toMember
- =<< retry x1 (query1 Cql.selectMember (params Quorum (cnv, usr)))
+ (toMember =<<)
+ <$> retry x1 (query1 Cql.selectMember (params Quorum (cnv, usr)))
remoteMemberLists ::
(MonadClient m) =>
@@ -807,15 +842,15 @@ memberLists ::
m [[LocalMember]]
memberLists convs = do
mems <- retry x1 $ query Cql.selectMembers (params Quorum (Identity convs))
- convMembers <- foldrM (\m acc -> liftA2 insert (mkMem m) (pure acc)) Map.empty mems
+ let convMembers = foldr (\m acc -> insert (mkMem m) acc) mempty mems
return $ map (\c -> fromMaybe [] (Map.lookup c convMembers)) convs
where
- insert Nothing acc = acc
- insert (Just (conv, mem)) acc =
+ insert (_, Nothing) acc = acc
+ insert (conv, Just mem) acc =
let f = (Just . maybe [mem] (mem :))
in Map.alter f conv acc
mkMem (cnv, usr, srv, prv, st, omus, omur, oar, oarr, hid, hidr, crn) =
- fmap (cnv,) <$> toMember (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 conv = join <$> memberLists [conv]
@@ -900,6 +935,20 @@ addLocalMembersToRemoteConv users qconv = do
class IsMemberUpdate mu where
updateMember :: MonadClient m => ConvId -> UserId -> mu -> m MemberUpdateData
+ updateMemberRemoteConv :: MonadClient m => Remote ConvId -> UserId -> mu -> m MemberUpdateData
+
+memberUpdateToData :: UserId -> MemberUpdate -> MemberUpdateData
+memberUpdateToData uid mup =
+ MemberUpdateData
+ { misTarget = Just uid,
+ misOtrMutedStatus = mupOtrMuteStatus mup,
+ misOtrMutedRef = mupOtrMuteRef mup,
+ misOtrArchived = mupOtrArchive mup,
+ misOtrArchivedRef = mupOtrArchiveRef mup,
+ misHidden = mupHidden mup,
+ misHiddenRef = mupHiddenRef mup,
+ misConvRoleName = Nothing
+ }
instance IsMemberUpdate MemberUpdate where
updateMember cid uid mup = do
@@ -912,17 +961,24 @@ instance IsMemberUpdate MemberUpdate where
addPrepQuery Cql.updateOtrMemberArchived (a, mupOtrArchiveRef mup, cid, uid)
for_ (mupHidden mup) $ \h ->
addPrepQuery Cql.updateMemberHidden (h, mupHiddenRef mup, cid, uid)
- return
- MemberUpdateData
- { misTarget = Just uid,
- misOtrMutedStatus = mupOtrMuteStatus mup,
- misOtrMutedRef = mupOtrMuteRef mup,
- misOtrArchived = mupOtrArchive mup,
- misOtrArchivedRef = mupOtrArchiveRef mup,
- misHidden = mupHidden mup,
- misHiddenRef = mupHiddenRef mup,
- misConvRoleName = Nothing
- }
+ pure (memberUpdateToData uid mup)
+ updateMemberRemoteConv (Tagged (Qualified cid domain)) uid mup = do
+ retry x5 . batch $ do
+ setType BatchUnLogged
+ setConsistency Quorum
+ for_ (mupOtrMuteStatus mup) $ \ms ->
+ addPrepQuery
+ Cql.updateRemoteOtrMemberMutedStatus
+ (ms, mupOtrMuteRef mup, domain, cid, uid)
+ for_ (mupOtrArchive mup) $ \a ->
+ addPrepQuery
+ Cql.updateRemoteOtrMemberArchived
+ (a, mupOtrArchiveRef mup, domain, cid, uid)
+ for_ (mupHidden mup) $ \h ->
+ addPrepQuery
+ Cql.updateRemoteMemberHidden
+ (h, mupHiddenRef mup, domain, cid, uid)
+ pure (memberUpdateToData uid mup)
instance IsMemberUpdate OtherMemberUpdate where
updateMember cid uid omu = do
@@ -943,6 +999,20 @@ instance IsMemberUpdate OtherMemberUpdate where
misConvRoleName = omuConvRoleName omu
}
+ -- FUTUREWORK: https://wearezeta.atlassian.net/browse/SQCORE-887
+ updateMemberRemoteConv _ _ _ =
+ pure
+ MemberUpdateData
+ { misTarget = Nothing,
+ misOtrMutedStatus = Nothing,
+ misOtrMutedRef = Nothing,
+ misOtrArchived = Nothing,
+ misOtrArchivedRef = Nothing,
+ misHidden = Nothing,
+ misHiddenRef = Nothing,
+ misConvRoleName = Nothing
+ }
+
-- | Select only the members of a remote conversation from a list of users.
-- Return the filtered list and a boolean indicating whether the all the input
-- users are members.
@@ -953,9 +1023,10 @@ filterRemoteConvMembers users (Qualified conv dom) =
<$> pooledMapConcurrentlyN 8 filterMember users
where
filterMember :: MonadClient m => UserId -> m [UserId]
- filterMember user = do
- let q = query Cql.selectRemoteConvMembership (params Quorum (user, dom, conv))
- map runIdentity <$> retry x1 q
+ filterMember user =
+ fmap (map (const user))
+ . retry x1
+ $ query Cql.selectRemoteConvMembers (params Quorum (user, dom, [conv]))
removeLocalMembersFromLocalConv ::
MonadClient m =>
@@ -1015,25 +1086,41 @@ removeMember usr cnv = retry x5 . batch $ do
addPrepQuery Cql.removeMember (cnv, usr)
addPrepQuery Cql.deleteUserConv (usr, cnv)
-newMember :: a -> InternalMember a
+newMember :: UserId -> LocalMember
newMember = flip newMemberWithRole roleNameWireAdmin
-newMemberWithRole :: a -> RoleName -> InternalMember a
+newMemberWithRole :: UserId -> RoleName -> LocalMember
newMemberWithRole u r =
- InternalMember
- { memId = u,
- memService = Nothing,
- memOtrMutedStatus = Nothing,
- memOtrMutedRef = Nothing,
- memOtrArchived = False,
- memOtrArchivedRef = Nothing,
- memHidden = False,
- memHiddenRef = Nothing,
- memConvRoleName = r
+ LocalMember
+ { lmId = u,
+ lmService = Nothing,
+ lmStatus = defMemberStatus,
+ lmConvRoleName = r
+ }
+
+toMemberStatus ::
+ ( -- otr muted
+ Maybe MutedStatus,
+ Maybe Text,
+ -- otr archived
+ Maybe Bool,
+ Maybe Text,
+ -- hidden
+ Maybe Bool,
+ Maybe Text
+ ) ->
+ MemberStatus
+toMemberStatus (omus, omur, oar, oarr, hid, hidr) =
+ MemberStatus
+ { msOtrMutedStatus = omus,
+ msOtrMutedRef = omur,
+ msOtrArchived = fromMaybe False oar,
+ msOtrArchivedRef = oarr,
+ msHidden = fromMaybe False hid,
+ msHiddenRef = hidr
}
toMember ::
- (Log.MonadLogger m, MonadThrow m) =>
( UserId,
Maybe ServiceId,
Maybe ProviderId,
@@ -1050,24 +1137,16 @@ toMember ::
-- conversation role name
Maybe RoleName
) ->
- m (Maybe LocalMember) -- FUTUREWORK: remove monad
-toMember (usr, srv, prv, sta, omus, omur, oar, oarr, hid, hidr, crn) =
- pure $
- if sta /= Just 0
- then Nothing
- else
- Just $
- InternalMember
- { memId = usr,
- memService = newServiceRef <$> srv <*> prv,
- memOtrMutedStatus = omus,
- memOtrMutedRef = omur,
- memOtrArchived = fromMaybe False oar,
- memOtrArchivedRef = oarr,
- memHidden = fromMaybe False hid,
- memHiddenRef = hidr,
- memConvRoleName = fromMaybe roleNameWireAdmin crn
- }
+ Maybe LocalMember
+toMember (usr, srv, prv, Just 0, omus, omur, oar, oarr, hid, hidr, crn) =
+ Just $
+ LocalMember
+ { lmId = usr,
+ lmService = newServiceRef <$> srv <*> prv,
+ lmStatus = toMemberStatus (omus, omur, oar, oarr, hid, hidr),
+ lmConvRoleName = fromMaybe roleNameWireAdmin crn
+ }
+toMember _ = Nothing
-- Clients ------------------------------------------------------------------
diff --git a/services/galley/src/Galley/Data/Queries.hs b/services/galley/src/Galley/Data/Queries.hs
index a54d3dcbf0..a07facd611 100644
--- a/services/galley/src/Galley/Data/Queries.hs
+++ b/services/galley/src/Galley/Data/Queries.hs
@@ -283,7 +283,6 @@ updateMemberConvRoleName = "update member set conversation_role = ? where conv =
-- Federated conversations -----------------------------------------------------
--
-- FUTUREWORK(federation): allow queries for pagination to support more than 500 (?) conversations for a user.
--- FUTUREWORK(federation): support other conversation attributes such as muted, archived, etc
-- local conversation with remote members
@@ -304,15 +303,26 @@ insertUserRemoteConv = "insert into user_remote_conv (user, conv_remote_domain,
selectUserRemoteConvs :: PrepQuery R (Identity UserId) (Domain, ConvId)
selectUserRemoteConvs = "select conv_remote_domain, conv_remote_id from user_remote_conv where user = ?"
-selectRemoteConvMembership :: PrepQuery R (UserId, Domain, ConvId) (Identity UserId)
-selectRemoteConvMembership = "select user from user_remote_conv where user = ? and conv_remote_domain = ? and conv_remote_id = ?"
-
-selectRemoteConvMembershipIn :: PrepQuery R (UserId, Domain, [ConvId]) (Identity ConvId)
-selectRemoteConvMembershipIn = "select conv_remote_id from user_remote_conv where user = ? and conv_remote_domain = ? and conv_remote_id in ?"
+selectRemoteConvMembers :: PrepQuery R (UserId, Domain, [ConvId]) (ConvId, Maybe MutedStatus, Maybe Text, Maybe Bool, Maybe Text, Maybe Bool, Maybe Text)
+selectRemoteConvMembers = "select conv_remote_id, otr_muted_status, otr_muted_ref, otr_archived, otr_archived_ref, hidden, hidden_ref from user_remote_conv where user = ? and conv_remote_domain = ? and conv_remote_id in ?"
deleteUserRemoteConv :: PrepQuery W (UserId, Domain, ConvId) ()
deleteUserRemoteConv = "delete from user_remote_conv where user = ? and conv_remote_domain = ? and conv_remote_id = ?"
+-- remote conversation status for local user
+
+updateRemoteOtrMemberMutedStatus :: PrepQuery W (MutedStatus, Maybe Text, Domain, ConvId, UserId) ()
+updateRemoteOtrMemberMutedStatus = "update user_remote_conv set otr_muted_status = ?, otr_muted_ref = ? where conv_remote_domain = ? and conv_remote_id = ? and user = ?"
+
+updateRemoteOtrMemberArchived :: PrepQuery W (Bool, Maybe Text, Domain, ConvId, UserId) ()
+updateRemoteOtrMemberArchived = "update user_remote_conv set otr_archived = ?, otr_archived_ref = ? where conv_remote_domain = ? and conv_remote_id = ? and user = ?"
+
+updateRemoteMemberHidden :: PrepQuery W (Bool, Maybe Text, Domain, ConvId, UserId) ()
+updateRemoteMemberHidden = "update user_remote_conv set hidden = ?, hidden_ref = ? where conv_remote_domain = ? and conv_remote_id = ? and user = ?"
+
+selectRemoteMemberStatus :: PrepQuery R (Domain, ConvId, UserId) (Maybe MutedStatus, Maybe Text, Maybe Bool, Maybe Text, Maybe Bool, Maybe Text)
+selectRemoteMemberStatus = "select otr_muted_status, otr_muted_ref, otr_archived, otr_archived_ref, hidden, hidden_ref from user_remote_conv where conv_remote_domain = ? and conv_remote_id = ? and user = ?"
+
-- Clients ------------------------------------------------------------------
selectClients :: PrepQuery R (Identity [UserId]) (UserId, C.Set ClientId)
diff --git a/services/galley/src/Galley/Data/Services.hs b/services/galley/src/Galley/Data/Services.hs
index 3ab4ab474f..0b633476bb 100644
--- a/services/galley/src/Galley/Data/Services.hs
+++ b/services/galley/src/Galley/Data/Services.hs
@@ -53,13 +53,13 @@ import Imports
newtype BotMember = BotMember {fromBotMember :: LocalMember}
newBotMember :: LocalMember -> Maybe BotMember
-newBotMember m = const (BotMember m) <$> memService m
+newBotMember m = const (BotMember m) <$> lmService m
botMemId :: BotMember -> BotId
-botMemId = BotId . memId . fromBotMember
+botMemId = BotId . lmId . fromBotMember
botMemService :: BotMember -> ServiceRef
-botMemService = fromJust . memService . fromBotMember
+botMemService = fromJust . lmService . fromBotMember
addBotMember :: Qualified UserId -> ServiceRef -> BotId -> ConvId -> UTCTime -> Galley (Event, BotMember)
addBotMember qorig s bot cnv now = do
@@ -77,7 +77,7 @@ addBotMember qorig s bot cnv now = do
localDomain = qDomain qorig
-- FUTUREWORK: support remote bots
e = Event MemberJoin qcnv qorig now (EdMembersJoin . SimpleMembers $ (fmap toSimpleMember [botUserId bot]))
- mem = (newMember (botUserId bot)) {memService = Just s}
+ mem = (newMember (botUserId bot)) {lmService = Just s}
toSimpleMember :: UserId -> SimpleMember
toSimpleMember u = SimpleMember (Qualified u localDomain) roleNameWireAdmin
diff --git a/services/galley/src/Galley/Intra/Push.hs b/services/galley/src/Galley/Intra/Push.hs
index 036ec3d856..04c2e48b3e 100644
--- a/services/galley/src/Galley/Intra/Push.hs
+++ b/services/galley/src/Galley/Intra/Push.hs
@@ -105,7 +105,7 @@ data RecipientBy user = Recipient
makeLenses ''RecipientBy
recipient :: LocalMember -> Recipient
-recipient = userRecipient . memId
+recipient = userRecipient . lmId
userRecipient :: user -> RecipientBy user
userRecipient u = Recipient u RecipientClientsAll
diff --git a/services/galley/test/integration/API.hs b/services/galley/test/integration/API.hs
index aede4721e2..289ee3c0b2 100644
--- a/services/galley/test/integration/API.hs
+++ b/services/galley/test/integration/API.hs
@@ -59,8 +59,10 @@ import Data.String.Conversions (cs)
import qualified Data.Text as T
import qualified Data.Text.Ascii as Ascii
import Data.Time.Clock (getCurrentTime)
+import Galley.API.Mapping
import Galley.Options (Opts, optFederator)
-import Galley.Types hiding (InternalMember (..))
+import Galley.Types hiding (LocalMember (..))
+import Galley.Types.Conversations.Members
import Galley.Types.Conversations.Roles
import qualified Galley.Types.Teams as Teams
import Gundeck.Types.Notification
@@ -79,7 +81,11 @@ import TestSetup
import Util.Options (Endpoint (Endpoint))
import Wire.API.Conversation
import qualified Wire.API.Federation.API.Brig as FederatedBrig
-import Wire.API.Federation.API.Galley (GetConversationsResponse (..))
+import Wire.API.Federation.API.Galley
+ ( GetConversationsResponse (..),
+ RemoteConvMembers (..),
+ RemoteConversation (..),
+ )
import qualified Wire.API.Federation.API.Galley as FederatedGalley
import qualified Wire.API.Federation.GRPC.Types as F
import qualified Wire.API.Message as Message
@@ -181,6 +187,10 @@ tests s =
test s "member update (otr archive)" putMemberOtrArchiveOk,
test s "member update (hidden)" putMemberHiddenOk,
test s "member update (everything b)" putMemberAllOk,
+ test s "remote conversation member update (otr mute)" putRemoteConvMemberOtrMuteOk,
+ test s "remote conversation member update (otr archive)" putRemoteConvMemberOtrArchiveOk,
+ test s "remote conversation member update (otr hidden)" putRemoteConvMemberHiddenOk,
+ test s "remote conversation member update (everything)" putRemoteConvMemberAllOk,
test s "conversation receipt mode update" putReceiptModeOk,
test s "send typing indicators" postTypingIndicators,
test s "leave connect conversation" leaveConnectConversation,
@@ -225,7 +235,7 @@ emptyFederatedGalley =
e s = throwError err501 {errBody = cs ("mock not implemented: " <> s)}
in FederatedGalley.Api
{ FederatedGalley.onConversationCreated = \_ _ -> e "onConversationCreated",
- FederatedGalley.getConversations = \_ -> e "getConversations",
+ FederatedGalley.getConversations = \_ _ -> e "getConversations",
FederatedGalley.onConversationMembershipsChanged = \_ _ -> e "onConversationMembershipsChanged",
FederatedGalley.leaveConversation = \_ _ -> e "leaveConversation",
FederatedGalley.onMessageSent = \_ _ -> e "onMessageSent",
@@ -880,7 +890,7 @@ postMessageQualifiedLocalOwningBackendFailedToSendClients = do
}
galleyApi =
emptyFederatedGalley
- { FederatedGalley.onMessageSent = \_ _ -> throwError err503 {errBody = "Down for maintanance."}
+ { FederatedGalley.onMessageSent = \_ _ -> throwError err503 {errBody = "Down for maintenance."}
}
(resp2, _requests) <- postProteusMessageQualifiedWithMockFederator aliceUnqualified aliceClient convId message "data" Message.MismatchReportAll brigApi galleyApi
@@ -914,7 +924,7 @@ postMessageQualifiedRemoteOwningBackendFailure = do
let galleyApi =
emptyFederatedGalley
- { FederatedGalley.sendMessage = \_ _ -> throwError err503 {errBody = "Down for maintanance."}
+ { FederatedGalley.sendMessage = \_ _ -> throwError err503 {errBody = "Down for maintenance."}
}
(resp2, _requests) <-
@@ -1773,13 +1783,24 @@ getConvQualifiedOk = do
accessConvMeta :: TestM ()
accessConvMeta = do
+ localDomain <- viewFederationDomain
g <- view tsGalley
alice <- randomUser
bob <- randomUser
chuck <- randomUser
connectUsers alice (list1 bob [chuck])
conv <- decodeConvId <$> postConv alice [bob, chuck] (Just "gossip") [] Nothing Nothing
- let meta = ConversationMeta conv RegularConv alice [InviteAccess] ActivatedAccessRole (Just "gossip") Nothing Nothing Nothing
+ let meta =
+ ConversationMetadata
+ (Qualified conv localDomain)
+ RegularConv
+ alice
+ [InviteAccess]
+ ActivatedAccessRole
+ (Just "gossip")
+ Nothing
+ Nothing
+ Nothing
get (g . paths ["i/conversations", toByteString' conv, "meta"] . zUser alice) !!! do
const 200 === statusCode
const (Just meta) === (decode <=< responseBody)
@@ -1874,26 +1895,19 @@ testGetQualifiedRemoteConv = do
let remoteDomain = Domain "far-away.example.com"
bobQ = Qualified bobId remoteDomain
remoteConvId = Qualified convId remoteDomain
- aliceAsOtherMember = OtherMember aliceQ Nothing roleNameWireAdmin
bobAsOtherMember = OtherMember bobQ Nothing roleNameWireAdmin
- aliceAsMember = Member aliceId Nothing Nothing Nothing False Nothing False Nothing roleNameWireAdmin
+ aliceAsLocal = LocalMember aliceId defMemberStatus Nothing roleNameWireAdmin
+ aliceAsOtherMember = localMemberToOther (qDomain aliceQ) aliceAsLocal
+ aliceAsSelfMember = localMemberToSelf aliceAsLocal
registerRemoteConv remoteConvId bobQ Nothing (Set.fromList [aliceAsOtherMember])
- let mockConversation =
- Conversation
- { cnvQualifiedId = remoteConvId,
- cnvType = RegularConv,
- cnvCreator = bobId,
- cnvAccess = [],
- cnvAccessRole = ActivatedAccessRole,
- cnvName = Just "federated gossip",
- cnvMembers = ConvMembers aliceAsMember [bobAsOtherMember],
- cnvTeam = Nothing,
- cnvMessageTimer = Nothing,
- cnvReceiptMode = Nothing
- }
+ let mockConversation = mkConv remoteConvId bobId roleNameWireAdmin [bobAsOtherMember]
remoteConversationResponse = GetConversationsResponse [mockConversation]
+ expected =
+ Conversation
+ (rcnvMetadata mockConversation)
+ (ConvMembers aliceAsSelfMember (rcmOthers (rcnvMembers mockConversation)))
opts <- view tsGConf
(respAll, _) <-
@@ -1904,9 +1918,7 @@ testGetQualifiedRemoteConv = do
(getConvQualified aliceId remoteConvId)
conv <- responseJsonUnsafe <$> (pure respAll (pure respAll (pure respAll maybeToList (remoteConversationView alice defMemberStatus mockConversationB)
+ <> [localConv]
actualFound = sortOn cnvQualifiedId $ crFound convs
assertEqual "found conversations" expectedFound actualFound
@@ -2578,6 +2581,34 @@ putMemberAllOk =
}
)
+putRemoteConvMemberOtrMuteOk :: TestM ()
+putRemoteConvMemberOtrMuteOk = do
+ putRemoteConvMemberOk (memberUpdate {mupOtrMuteStatus = Just 1, mupOtrMuteRef = Just "ref"})
+ putRemoteConvMemberOk (memberUpdate {mupOtrMuteStatus = Just 0})
+
+putRemoteConvMemberOtrArchiveOk :: TestM ()
+putRemoteConvMemberOtrArchiveOk = do
+ putRemoteConvMemberOk (memberUpdate {mupOtrArchive = Just True, mupOtrArchiveRef = Just "ref"})
+ putRemoteConvMemberOk (memberUpdate {mupOtrArchive = Just False})
+
+putRemoteConvMemberHiddenOk :: TestM ()
+putRemoteConvMemberHiddenOk = do
+ putRemoteConvMemberOk (memberUpdate {mupHidden = Just True, mupHiddenRef = Just "ref"})
+ putRemoteConvMemberOk (memberUpdate {mupHidden = Just False})
+
+putRemoteConvMemberAllOk :: TestM ()
+putRemoteConvMemberAllOk =
+ putRemoteConvMemberOk
+ ( memberUpdate
+ { mupOtrMuteStatus = Just 0,
+ mupOtrMuteRef = Just "mref",
+ mupOtrArchive = Just True,
+ mupOtrArchiveRef = Just "aref",
+ mupHidden = Just True,
+ mupHiddenRef = Just "href"
+ }
+ )
+
putMemberOk :: MemberUpdate -> TestM ()
putMemberOk update = do
c <- view tsCannon
@@ -2603,7 +2634,7 @@ putMemberOk update = do
}
-- Update member state & verify push notification
WS.bracketR c bob $ \ws -> do
- putMember bob update conv !!! const 200 === statusCode
+ putMember bob update qconv !!! const 200 === statusCode
void . liftIO . WS.assertMatch (5 # Second) ws $ \n -> do
let e = List1.head (WS.unpackPayload n)
ntfTransient n @?= False
@@ -2633,6 +2664,92 @@ putMemberOk update = do
assertEqual "hidden" (memHidden memberBob) (memHidden newBob)
assertEqual "hidden_ref" (memHiddenRef memberBob) (memHiddenRef newBob)
+putRemoteConvMemberOk :: MemberUpdate -> TestM ()
+putRemoteConvMemberOk update = do
+ c <- view tsCannon
+ qalice <- randomQualifiedUser
+ let alice = qUnqualified qalice
+
+ -- create a remote conversation with alice
+ let remoteDomain = Domain "bobland.example.com"
+ qbob <- Qualified <$> randomId <*> pure remoteDomain
+ qconv <- Qualified <$> randomId <*> pure remoteDomain
+ fedGalleyClient <- view tsFedGalleyClient
+ now <- liftIO getCurrentTime
+ let cmu =
+ FederatedGalley.ConversationMemberUpdate
+ { cmuTime = now,
+ cmuOrigUserId = qbob,
+ cmuConvId = qUnqualified qconv,
+ cmuAlreadyPresentUsers = [],
+ cmuAction =
+ FederatedGalley.ConversationMembersActionAdd (pure (qalice, roleNameWireMember))
+ }
+ FederatedGalley.onConversationMembershipsChanged fedGalleyClient remoteDomain cmu
+
+ -- Expected member state
+ let memberAlice =
+ Member
+ { memId = alice,
+ memService = Nothing,
+ memOtrMutedStatus = mupOtrMuteStatus update,
+ memOtrMutedRef = mupOtrMuteRef update,
+ memOtrArchived = Just True == mupOtrArchive update,
+ memOtrArchivedRef = mupOtrArchiveRef update,
+ memHidden = Just True == mupHidden update,
+ memHiddenRef = mupHiddenRef update,
+ memConvRoleName = roleNameWireMember
+ }
+ -- Update member state & verify push notification
+ WS.bracketR c alice $ \ws -> do
+ putMember alice update qconv !!! const 200 === statusCode
+ void . liftIO . WS.assertMatch (5 # Second) ws $ \n -> do
+ let e = List1.head (WS.unpackPayload n)
+ ntfTransient n @?= False
+ evtConv e @?= qconv
+ evtType e @?= MemberStateUpdate
+ evtFrom e @?= qalice
+ case evtData e of
+ EdMemberUpdate mis -> do
+ assertEqual "otr_muted_status" (mupOtrMuteStatus update) (misOtrMutedStatus mis)
+ assertEqual "otr_muted_ref" (mupOtrMuteRef update) (misOtrMutedRef mis)
+ assertEqual "otr_archived" (mupOtrArchive update) (misOtrArchived mis)
+ assertEqual "otr_archived_ref" (mupOtrArchiveRef update) (misOtrArchivedRef mis)
+ assertEqual "hidden" (mupHidden update) (misHidden mis)
+ assertEqual "hidden_ref" (mupHiddenRef update) (misHiddenRef mis)
+ x -> assertFailure $ "Unexpected event data: " ++ show x
+
+ -- Fetch remote conversation
+ let bobAsLocal = LocalMember (qUnqualified qbob) defMemberStatus Nothing roleNameWireAdmin
+ let mockConversation =
+ mkConv
+ qconv
+ (qUnqualified qbob)
+ roleNameWireMember
+ [localMemberToOther remoteDomain bobAsLocal]
+ remoteConversationResponse = GetConversationsResponse [mockConversation]
+ opts <- view tsGConf
+ (rs, _) <-
+ withTempMockFederator
+ opts
+ remoteDomain
+ (const remoteConversationResponse)
+ $ getConvQualified alice qconv
+ responseJsonUnsafe rs
+ liftIO $ do
+ assertBool "user" (isJust alice')
+ let newAlice = fromJust alice'
+ assertEqual "id" (memId memberAlice) (memId newAlice)
+ assertEqual "otr_muted_status" (memOtrMutedStatus memberAlice) (memOtrMutedStatus newAlice)
+ assertEqual "otr_muted_ref" (memOtrMutedRef memberAlice) (memOtrMutedRef newAlice)
+ assertEqual "otr_archived" (memOtrArchived memberAlice) (memOtrArchived newAlice)
+ assertEqual "otr_archived_ref" (memOtrArchivedRef memberAlice) (memOtrArchivedRef newAlice)
+ assertEqual "hidden" (memHidden memberAlice) (memHidden newAlice)
+ assertEqual "hidden_ref" (memHiddenRef memberAlice) (memHiddenRef newAlice)
+
putReceiptModeOk :: TestM ()
putReceiptModeOk = do
c <- view tsCannon
diff --git a/services/galley/test/integration/API/Federation.hs b/services/galley/test/integration/API/Federation.hs
index 1cb7090568..0daa10e4c5 100644
--- a/services/galley/test/integration/API/Federation.hs
+++ b/services/galley/test/integration/API/Federation.hs
@@ -48,8 +48,9 @@ import qualified Test.Tasty.Cannon as WS
import Test.Tasty.HUnit
import TestHelpers
import TestSetup
+import Wire.API.Conversation.Member (Member (..))
import Wire.API.Conversation.Role
-import Wire.API.Federation.API.Galley (GetConversationsRequest (..), GetConversationsResponse (..))
+import Wire.API.Federation.API.Galley (GetConversationsRequest (..), GetConversationsResponse (..), RemoteConvMembers (..), RemoteConversation (..))
import qualified Wire.API.Federation.API.Galley as FedGalley
import qualified Wire.API.Federation.GRPC.Types as F
import Wire.API.Message (ClientMismatchStrategy (..), MessageSendingStatus (mssDeletedClients, mssFailedToSend, mssRedundantClients), mkQualifiedOtrPayload, mssMissingClients)
@@ -73,45 +74,51 @@ tests s =
getConversationsAllFound :: TestM ()
getConversationsAllFound = do
- -- FUTUREWORK: make alice / bob remote users
- [alice, bob] <- randomUsers 2
- connectUsers alice (singleton bob)
- -- create & get one2one conv
- cnv1 <- responseJsonUnsafeWithMsg "conversation" <$> postO2OConv alice bob (Just "gossip1")
- getConvs alice (Just $ Left [qUnqualified . cnvQualifiedId $ cnv1]) Nothing !!! do
- const 200 === statusCode
- const (Just [cnvQualifiedId cnv1]) === fmap (map cnvQualifiedId . convList) . responseJsonUnsafe
+ bob <- randomUser
+
-- create & get group conv
- carl <- randomUser
- connectUsers alice (singleton carl)
- cnv2 <- responseJsonUnsafeWithMsg "conversation" <$> postConv alice [bob, carl] (Just "gossip2") [] Nothing Nothing
- getConvs alice (Just $ Left [qUnqualified . cnvQualifiedId $ cnv2]) Nothing !!! do
+ aliceQ <- Qualified <$> randomId <*> pure (Domain "far-away.example.com")
+ carlQ <- randomQualifiedUser
+ connectUsers bob (singleton (qUnqualified carlQ))
+
+ cnv2 <-
+ responseJsonError
+ =<< postConvWithRemoteUser (qDomain aliceQ) (mkProfile aliceQ (Name "alice")) bob [aliceQ, carlQ]
+
+ getConvs bob (Just $ Left [qUnqualified (cnvQualifiedId cnv2)]) Nothing !!! do
const 200 === statusCode
- const (Just [cnvQualifiedId cnv2]) === fmap (map cnvQualifiedId . convList) . responseJsonUnsafe
- -- get both
+ const (Just (Just [cnvQualifiedId cnv2]))
+ === fmap (fmap (map cnvQualifiedId . convList)) . responseJsonMaybe
+
+ -- FUTUREWORK: also create a one2one conversation
+
+ -- get conversations
fedGalleyClient <- view tsFedGalleyClient
- localDomain <- viewFederationDomain
- let aliceQualified = Qualified alice localDomain
GetConversationsResponse cs <-
FedGalley.getConversations
fedGalleyClient
- (GetConversationsRequest aliceQualified $ qUnqualified . cnvQualifiedId <$> [cnv1, cnv2])
- let c1 = find ((== cnvQualifiedId cnv1) . cnvQualifiedId) cs
- let c2 = find ((== cnvQualifiedId cnv2) . cnvQualifiedId) cs
- liftIO . forM_ [(cnv1, c1), (cnv2, c2)] $ \(expected, actual) -> do
+ (qDomain aliceQ)
+ ( GetConversationsRequest
+ (qUnqualified aliceQ)
+ (map (qUnqualified . cnvQualifiedId) [cnv2])
+ )
+
+ let c2 = find ((== cnvQualifiedId cnv2) . cnvmQualifiedId . rcnvMetadata) cs
+
+ liftIO $ do
assertEqual
"name mismatch"
- (Just $ cnvName expected)
- (cnvName <$> actual)
+ (Just $ cnvName cnv2)
+ (cnvmName . rcnvMetadata <$> c2)
assertEqual
- "self member mismatch"
- (Just . cmSelf $ cnvMembers expected)
- (cmSelf . cnvMembers <$> actual)
+ "self member role mismatch"
+ (Just . memConvRoleName . cmSelf $ cnvMembers cnv2)
+ (rcmSelfRole . rcnvMembers <$> c2)
assertEqual
"other members mismatch"
- (Just [])
- ((\c -> cmOthers (cnvMembers c) \\ cmOthers (cnvMembers expected)) <$> actual)
+ (Just (sort [bob, qUnqualified carlQ]))
+ (fmap (sort . map (qUnqualified . omQualifiedId) . rcmOthers . rcnvMembers) c2)
getConversationsNotPartOf :: TestM ()
getConversationsNotPartOf = do
@@ -127,11 +134,11 @@ getConversationsNotPartOf = do
fedGalleyClient <- view tsFedGalleyClient
localDomain <- viewFederationDomain
rando <- Id <$> liftIO nextRandom
- let randoQualified = Qualified rando localDomain
GetConversationsResponse cs <-
FedGalley.getConversations
fedGalleyClient
- (GetConversationsRequest randoQualified [qUnqualified . cnvQualifiedId $ cnv1])
+ localDomain
+ (GetConversationsRequest rando [qUnqualified . cnvQualifiedId $ cnv1])
liftIO $ assertEqual "conversation list not empty" [] cs
addLocalUser :: TestM ()
diff --git a/services/galley/test/integration/API/Roles.hs b/services/galley/test/integration/API/Roles.hs
index 72928b1333..ae5d11d529 100644
--- a/services/galley/test/integration/API/Roles.hs
+++ b/services/galley/test/integration/API/Roles.hs
@@ -154,6 +154,7 @@ wireAdminChecks ::
TestM ()
wireAdminChecks cid admin otherAdmin mem = do
let role = roleNameWireAdmin
+ qcid <- Qualified cid <$> viewFederationDomain
other <- randomUser
connectUsers admin (singleton other)
-- Admins can perform all operations on the conversation; creator is not relevant
@@ -183,7 +184,7 @@ wireAdminChecks cid admin otherAdmin mem = do
putAccessUpdate admin cid activatedAccess !!! assertActionSucceeded
-- Update your own member state
let memUpdate = memberUpdate {mupOtrArchive = Just True}
- putMember admin memUpdate cid !!! assertActionSucceeded
+ putMember admin memUpdate qcid !!! assertActionSucceeded
-- You can also leave a conversation
deleteMemberUnqualified admin admin cid !!! assertActionSucceeded
-- Readding the user
@@ -199,6 +200,7 @@ wireMemberChecks ::
TestM ()
wireMemberChecks cid mem admin otherMem = do
let role = roleNameWireMember
+ qcid <- Qualified cid <$> viewFederationDomain
other <- randomUser
connectUsers mem (singleton other)
-- Members cannot perform pretty much any action on the conversation
@@ -227,7 +229,7 @@ wireMemberChecks cid mem admin otherMem = do
-- Update your own member state
let memUpdate = memberUpdate {mupOtrArchive = Just True}
- putMember mem memUpdate cid !!! assertActionSucceeded
+ putMember mem memUpdate qcid !!! assertActionSucceeded
-- Last option is to leave a conversation
deleteMemberUnqualified mem mem cid !!! assertActionSucceeded
-- Let's readd the user to make tests easier
diff --git a/services/galley/test/integration/API/Util.hs b/services/galley/test/integration/API/Util.hs
index aaea6c8a7b..1fd5a93040 100644
--- a/services/galley/test/integration/API/Util.hs
+++ b/services/galley/test/integration/API/Util.hs
@@ -70,7 +70,7 @@ import Data.UUID.V4
import Galley.Intra.User (chunkify)
import qualified Galley.Options as Opts
import qualified Galley.Run as Run
-import Galley.Types hiding (InternalMember, MemberJoin, MemberLeave, memConvRoleName, memId, memOtrArchived, memOtrArchivedRef, memOtrMutedRef)
+import Galley.Types
import qualified Galley.Types as Conv
import Galley.Types.Conversations.Roles hiding (DeleteConversation)
import Galley.Types.Teams hiding (Event, EventType (..))
@@ -107,7 +107,6 @@ import Util.Options
import Web.Cookie
import Wire.API.Conversation
import qualified Wire.API.Conversation as Public
-import Wire.API.Event.Team (EventType (MemberJoin, MemberLeave, TeamDelete, TeamUpdate))
import qualified Wire.API.Event.Team as TE
import qualified Wire.API.Federation.API.Brig as FederatedBrig
import qualified Wire.API.Federation.API.Galley as FederatedGalley
@@ -945,12 +944,12 @@ getSelfMember u c = do
. zConn "conn"
. zType "access"
-putMember :: UserId -> MemberUpdate -> ConvId -> TestM ResponseLBS
-putMember u m c = do
+putMember :: UserId -> MemberUpdate -> Qualified ConvId -> TestM ResponseLBS
+putMember u m (Qualified c dom) = do
g <- view tsGalley
put $
g
- . paths ["conversations", toByteString' c, "self"]
+ . paths ["conversations", toByteString' dom, toByteString' c, "self"]
. zUser u
. zConn "conn"
. zType "access"
@@ -1868,20 +1867,26 @@ someLastPrekeys =
lastPrekey "pQABARn//wKhAFgg1rZEY6vbAnEz+Ern5kRny/uKiIrXTb/usQxGnceV2HADoQChAFgglacihnqg/YQJHkuHNFU7QD6Pb3KN4FnubaCF2EVOgRkE9g=="
]
-mkConv :: Qualified ConvId -> UserId -> Member -> [OtherMember] -> Conversation
-mkConv cnvId creator selfMember otherMembers =
- Conversation
- { cnvQualifiedId = cnvId,
- cnvType = RegularConv,
- cnvCreator = creator,
- cnvAccess = [],
- cnvAccessRole = ActivatedAccessRole,
- cnvName = Just "federated gossip",
- cnvMembers = ConvMembers selfMember otherMembers,
- cnvTeam = Nothing,
- cnvMessageTimer = Nothing,
- cnvReceiptMode = Nothing
- }
+mkConv ::
+ Qualified ConvId ->
+ UserId ->
+ RoleName ->
+ [OtherMember] ->
+ FederatedGalley.RemoteConversation
+mkConv cnvId creator selfRole otherMembers =
+ FederatedGalley.RemoteConversation
+ ( ConversationMetadata
+ cnvId
+ RegularConv
+ creator
+ []
+ ActivatedAccessRole
+ (Just "federated gossip")
+ Nothing
+ Nothing
+ Nothing
+ )
+ (FederatedGalley.RemoteConvMembers selfRole otherMembers)
-- | ES is only refreshed occasionally; we don't want to wait for that in tests.
refreshIndex :: TestM ()
@@ -2183,7 +2188,7 @@ checkTeamMemberJoin :: HasCallStack => TeamId -> UserId -> WS.WebSocket -> TestM
checkTeamMemberJoin tid uid w = WS.awaitMatch_ checkTimeout w $ \notif -> do
ntfTransient notif @?= False
let e = List1.head (WS.unpackPayload notif)
- e ^. eventType @?= MemberJoin
+ e ^. eventType @?= TE.MemberJoin
e ^. eventTeam @?= tid
e ^. eventData @?= Just (EdMemberJoin uid)
@@ -2191,7 +2196,7 @@ checkTeamMemberLeave :: HasCallStack => TeamId -> UserId -> WS.WebSocket -> Test
checkTeamMemberLeave tid usr w = WS.assertMatch_ checkTimeout w $ \notif -> do
ntfTransient notif @?= False
let e = List1.head (WS.unpackPayload notif)
- e ^. eventType @?= MemberLeave
+ e ^. eventType @?= TE.MemberLeave
e ^. eventTeam @?= tid
e ^. eventData @?= Just (EdMemberLeave usr)
@@ -2199,7 +2204,7 @@ checkTeamUpdateEvent :: (HasCallStack, MonadIO m, MonadCatch m) => TeamId -> Tea
checkTeamUpdateEvent tid upd w = WS.assertMatch_ checkTimeout w $ \notif -> do
ntfTransient notif @?= False
let e = List1.head (WS.unpackPayload notif)
- e ^. eventType @?= TeamUpdate
+ e ^. eventType @?= TE.TeamUpdate
e ^. eventTeam @?= tid
e ^. eventData @?= Just (EdTeamUpdate upd)
@@ -2216,7 +2221,7 @@ checkTeamDeleteEvent :: HasCallStack => TeamId -> WS.WebSocket -> TestM ()
checkTeamDeleteEvent tid w = WS.assertMatch_ checkTimeout w $ \notif -> do
ntfTransient notif @?= False
let e = List1.head (WS.unpackPayload notif)
- e ^. eventType @?= TeamDelete
+ e ^. eventType @?= TE.TeamDelete
e ^. eventTeam @?= tid
e ^. eventData @?= Nothing
diff --git a/services/galley/test/unit/Test/Galley/Mapping.hs b/services/galley/test/unit/Test/Galley/Mapping.hs
index 2f77de53b2..f48ac6d312 100644
--- a/services/galley/test/unit/Test/Galley/Mapping.hs
+++ b/services/galley/test/unit/Test/Galley/Mapping.hs
@@ -23,184 +23,139 @@ module Test.Galley.Mapping where
import Data.Domain
import Data.Id
import Data.Qualified
-import Galley.API ()
+import Data.Tagged
import Galley.API.Mapping
import qualified Galley.Data as Data
-import Galley.Types (LocalMember, RemoteMember)
-import qualified Galley.Types.Conversations.Members as I
+import Galley.Types.Conversations.Members
import Imports
import Test.Tasty
-import Test.Tasty.HUnit
+import Test.Tasty.QuickCheck
+-- import Test.Tasty.HUnit
import Wire.API.Conversation
-import Wire.API.Conversation.Role (roleNameWireAdmin)
+import Wire.API.Conversation.Role
+import Wire.API.Federation.API.Galley
+ ( RemoteConvMembers (..),
+ RemoteConversation (..),
+ )
tests :: TestTree
tests =
testGroup
"ConversationMapping"
- [ testCase "Alice@A Conv@A" runMappingSimple,
- testCase "Alice@A Conv@A requester=not a member@A" runMappingNotAMemberA,
- testCase "Alice@A Conv@A requester=not a member@B" runMappingNotAMemberB,
- testCase "Alice@A Conv@A Bob@B" runMappingRemoteUser,
- testCase "Alice@A Conv@B Bob@B" runMappingRemoteConv,
- testCase "Alice@A Conv@B Bob@B bobUUID=aliceUUID" runMappingSameUnqualifiedUUID
+ [ testProperty "conversation view for a valid user is non-empty" $
+ \(ConvWithLocalUser c uid) dom -> isJust (conversationViewMaybe dom uid c),
+ testProperty "self user in conversation view is correct" $
+ \(ConvWithLocalUser c uid) dom ->
+ fmap (memId . cmSelf . cnvMembers) (conversationViewMaybe dom uid c)
+ == Just uid,
+ testProperty "conversation view metadata is correct" $
+ \(ConvWithLocalUser c uid) dom ->
+ fmap cnvMetadata (conversationViewMaybe dom uid c)
+ == Just (Data.convMetadata dom c),
+ testProperty "other members in conversation view do not contain self" $
+ \(ConvWithLocalUser c uid) dom -> case conversationViewMaybe dom uid c of
+ Nothing -> False
+ Just cnv ->
+ not
+ ( Qualified uid dom
+ `elem` (map omQualifiedId (cmOthers (cnvMembers cnv)))
+ ),
+ testProperty "conversation view contains all users" $
+ \(ConvWithLocalUser c uid) dom ->
+ fmap (sort . cnvUids dom) (conversationViewMaybe dom uid c)
+ == Just (sort (convUids dom c)),
+ testProperty "conversation view for an invalid user is empty" $
+ \(RandomConversation c) dom uid ->
+ not (elem uid (map lmId (Data.convLocalMembers c)))
+ ==> isNothing (conversationViewMaybe dom uid c),
+ testProperty "remote conversation view for a valid user is non-empty" $
+ \(ConvWithRemoteUser c ruid) dom ->
+ qDomain (unTagged ruid) /= dom
+ ==> isJust (conversationToRemote dom ruid c),
+ testProperty "self user role in remote conversation view is correct" $
+ \(ConvWithRemoteUser c ruid) dom ->
+ qDomain (unTagged ruid) /= dom
+ ==> fmap (rcmSelfRole . rcnvMembers) (conversationToRemote dom ruid c)
+ == Just roleNameWireMember,
+ testProperty "remote conversation view metadata is correct" $
+ \(ConvWithRemoteUser c ruid) dom ->
+ qDomain (unTagged ruid) /= dom
+ ==> fmap (rcnvMetadata) (conversationToRemote dom ruid c)
+ == Just (Data.convMetadata dom c),
+ testProperty "remote conversation view does not contain self" $
+ \(ConvWithRemoteUser c ruid) dom -> case conversationToRemote dom ruid c of
+ Nothing -> False
+ Just rcnv ->
+ not
+ ( unTagged ruid
+ `elem` (map omQualifiedId (rcmOthers (rcnvMembers rcnv)))
+ )
]
-runMappingSimple :: HasCallStack => IO ()
-runMappingSimple = do
- let convDomain = Domain "backendA.example.com"
- let userDomain = Domain "backendA.example.com"
- alice <- randomId
- let requester = Qualified alice userDomain
- let expectedSelf = Just $ mkMember requester
- let expectedOthers = Just []
-
- let locals = [mkInternalMember requester]
- let remotes = []
- conv <- mkInternalConv locals remotes
- let actual = cnvMembers <$> conversationViewMaybeQualified convDomain requester conv
-
- assertEqual "self:" expectedSelf (cmSelf <$> actual)
- assertEqual "others:" expectedOthers (cmOthers <$> actual)
-
-runMappingNotAMemberA :: HasCallStack => IO ()
-runMappingNotAMemberA = do
- let convDomain = Domain "backendA.example.com"
- let aliceDomain = Domain "backendA.example.com"
- alice <- flip Qualified aliceDomain <$> randomId
- requester <- flip Qualified aliceDomain <$> randomId
-
- let locals = [mkInternalMember alice]
- let remotes = []
- conv <- mkInternalConv locals remotes
- let actual = cnvMembers <$> conversationViewMaybeQualified convDomain requester conv
-
- assertEqual "members:" Nothing actual
-
-runMappingNotAMemberB :: HasCallStack => IO ()
-runMappingNotAMemberB = do
- let convDomain = Domain "backendA.example.com"
- let aliceDomain = Domain "backendA.example.com"
- let requesterDomain = Domain "backendB.example.com"
- alice <- flip Qualified aliceDomain <$> randomId
- requester <- flip Qualified requesterDomain <$> randomId
-
- let locals = [mkInternalMember alice]
- let remotes = []
- conv <- mkInternalConv locals remotes
- let actual = cnvMembers <$> conversationViewMaybeQualified convDomain requester conv
-
- assertEqual "members:" Nothing actual
-
-runMappingRemoteUser :: HasCallStack => IO ()
-runMappingRemoteUser = do
- let aliceDomain = Domain "backendA.example.com"
- let convDomain = Domain "backendA.example.com"
- let bobDomain = Domain "backendB.example.com"
- alice <- flip Qualified aliceDomain <$> randomId
- bob <- flip Qualified bobDomain <$> randomId
- let expectedSelf = Just $ mkMember alice
- let expectedOthers = Just [mkOtherMember bob]
-
- let locals = [mkInternalMember alice]
- let remotes = [mkRemoteMember bob]
- conv <- mkInternalConv locals remotes
- let actual = cnvMembers <$> conversationViewMaybeQualified convDomain alice conv
-
- assertEqual "self:" expectedSelf (cmSelf <$> actual)
- assertEqual "others:" expectedOthers (cmOthers <$> actual)
-
-runMappingRemoteConv :: HasCallStack => IO ()
-runMappingRemoteConv = do
- let aliceDomain = Domain "backendA.example.com"
- let convDomain = Domain "backendB.example.com"
- let bobDomain = Domain "backendB.example.com"
- alice <- flip Qualified aliceDomain <$> randomId
- bob <- flip Qualified bobDomain <$> randomId
- let expectedSelf = Just $ mkMember alice
- let expectedOthers = Just [mkOtherMember bob]
-
- let locals = [mkInternalMember bob]
- let remotes = [mkRemoteMember alice]
- conv <- mkInternalConv locals remotes
- let actual = cnvMembers <$> conversationViewMaybeQualified convDomain alice conv
-
- assertEqual "self:" expectedSelf (cmSelf <$> actual)
- assertEqual "others:" expectedOthers (cmOthers <$> actual)
-
--- Here we expect the conversationView to return nothing, because Alice (the
--- requester) is not part of the conversation (Her unqualified UUID is part of
--- the conversation, but the function should catch this possibly malicious
--- edge case)
-runMappingSameUnqualifiedUUID :: HasCallStack => IO ()
-runMappingSameUnqualifiedUUID = do
- let aliceDomain = Domain "backendA.example.com"
- let convDomain = Domain "backendB.example.com"
- let bobDomain = Domain "backendB.example.com"
- uuid <- randomId
- let alice = Qualified uuid aliceDomain
- let bob = Qualified uuid bobDomain
-
- let locals = [mkInternalMember bob]
- let remotes = []
- conv <- mkInternalConv locals remotes
- let actual = cnvMembers <$> conversationViewMaybeQualified convDomain alice conv
-
- assertEqual "members:" Nothing actual
-
---------------------------------------------------------------
-
-mkOtherMember :: Qualified UserId -> OtherMember
-mkOtherMember u = OtherMember u Nothing roleNameWireAdmin
-
-mkRemoteMember :: Qualified UserId -> RemoteMember
-mkRemoteMember u = I.RemoteMember (toRemote u) roleNameWireAdmin
-
-mkInternalConv :: [LocalMember] -> [RemoteMember] -> IO Data.Conversation
-mkInternalConv locals remotes = do
- -- for the conversationView unit tests, the creator plays no importance, so for simplicity this is set to a random value.
- creator <- randomId
- cnv <- randomId
- pure $
- Data.Conversation
- { Data.convId = cnv,
- Data.convType = RegularConv,
- Data.convCreator = creator,
- Data.convName = Just "unit testing gossip",
- Data.convAccess = [],
- Data.convAccessRole = ActivatedAccessRole,
- Data.convLocalMembers = locals,
- Data.convRemoteMembers = remotes,
- Data.convTeam = Nothing,
- Data.convDeleted = Just False,
- Data.convMessageTimer = Nothing,
- Data.convReceiptMode = Nothing
- }
-
-mkMember :: Qualified UserId -> Member
-mkMember (Qualified userId _domain) =
- Member
- { memId = userId,
- memService = Nothing,
- memOtrMutedStatus = Nothing,
- memOtrMutedRef = Nothing,
- memOtrArchived = False,
- memOtrArchivedRef = Nothing,
- memHidden = False,
- memHiddenRef = Nothing,
- memConvRoleName = roleNameWireAdmin
- }
-
-mkInternalMember :: Qualified UserId -> LocalMember
-mkInternalMember (Qualified userId _domain) =
- I.InternalMember
- { I.memId = userId,
- I.memService = Nothing,
- I.memOtrMutedStatus = Nothing,
- I.memOtrMutedRef = Nothing,
- I.memOtrArchived = False,
- I.memOtrArchivedRef = Nothing,
- I.memHidden = False,
- I.memHiddenRef = Nothing,
- I.memConvRoleName = roleNameWireAdmin
- }
+cnvUids :: Domain -> Conversation -> [Qualified UserId]
+cnvUids dom c =
+ let mems = cnvMembers c
+ in Qualified (memId (cmSelf mems)) dom :
+ map omQualifiedId (cmOthers mems)
+
+convUids :: Domain -> Data.Conversation -> [Qualified UserId]
+convUids dom c =
+ map ((`Qualified` dom) . lmId) (Data.convLocalMembers c)
+ <> map (unTagged . rmId) (Data.convRemoteMembers c)
+
+genLocalMember :: Gen LocalMember
+genLocalMember =
+ LocalMember
+ <$> arbitrary
+ <*> pure defMemberStatus
+ <*> pure Nothing
+ <*> arbitrary
+
+genRemoteMember :: Gen RemoteMember
+genRemoteMember = RemoteMember <$> arbitrary <*> pure roleNameWireMember
+
+genConversation :: [LocalMember] -> [RemoteMember] -> Gen Data.Conversation
+genConversation locals remotes =
+ Data.Conversation
+ <$> arbitrary
+ <*> pure RegularConv
+ <*> arbitrary
+ <*> arbitrary
+ <*> pure []
+ <*> pure ActivatedAccessRole
+ <*> pure locals
+ <*> pure remotes
+ <*> pure Nothing
+ <*> pure (Just False)
+ <*> pure Nothing
+ <*> pure Nothing
+
+newtype RandomConversation = RandomConversation Data.Conversation
+ deriving (Show)
+
+instance Arbitrary RandomConversation where
+ arbitrary =
+ RandomConversation <$> do
+ locals <- listOf genLocalMember
+ remotes <- listOf genRemoteMember
+ genConversation locals remotes
+
+data ConvWithLocalUser = ConvWithLocalUser Data.Conversation UserId
+ deriving (Show)
+
+instance Arbitrary ConvWithLocalUser where
+ arbitrary = do
+ RandomConversation conv <- arbitrary
+ member <- genLocalMember
+ let conv' = conv {Data.convLocalMembers = member : Data.convLocalMembers conv}
+ pure $ ConvWithLocalUser conv' (lmId member)
+
+data ConvWithRemoteUser = ConvWithRemoteUser Data.Conversation (Remote UserId)
+ deriving (Show)
+
+instance Arbitrary ConvWithRemoteUser where
+ arbitrary = do
+ RandomConversation conv <- arbitrary
+ member <- genRemoteMember
+ let conv' = conv {Data.convRemoteMembers = member : Data.convRemoteMembers conv}
+ pure $ ConvWithRemoteUser conv' (rmId member)