Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions charts/nginz/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,10 @@ nginx_conf:
- all
disable_zauth: true
unlimited_requests_endpoint: true
- path: /users/public/([^/]*)
envs:
- all
disable_zauth: true
- path: /users
envs:
- all
Expand Down
29 changes: 29 additions & 0 deletions integration/test/API/Galley.hs
Original file line number Diff line number Diff line change
Expand Up @@ -649,6 +649,35 @@ updateMessageTimer user qcnv update = do
req <- baseRequest user Galley Versioned path
submit "PUT" (addJSONObject ["message_timer" .= updateReq] req)

getConversationDescription ::
(HasCallStack, MakesValue user, MakesValue conv) =>
user ->
conv ->
App Response
getConversationDescription user qcnv = do
(cnvDomain, cnvId) <- objQid qcnv
req <- baseRequest user Galley Versioned (joinHttpPath ["conversations", cnvDomain, cnvId, "description"])
submit "GET" req

updateConversationDescription ::
(HasCallStack, MakesValue user, MakesValue conv) =>
user ->
conv ->
Int64 ->
ByteString ->
App Response
updateConversationDescription user qcnv baseVersion ciphertext = do
(cnvDomain, cnvId) <- objQid qcnv
req <- baseRequest user Galley Versioned (joinHttpPath ["conversations", cnvDomain, cnvId, "description"])
submit
"PUT"
( req
& addJSONObject
[ "base_version" .= baseVersion,
"ciphertext" .= BS.unpack (B64.encode ciphertext)
]
)

updateHistory ::
( HasCallStack,
MakesValue user,
Expand Down
4 changes: 4 additions & 0 deletions integration/test/Notifications.hs
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,10 @@ isConvMsgTimerUpdateNotif :: (HasCallStack, MakesValue n) => n -> App Bool
isConvMsgTimerUpdateNotif n =
fieldEquals n "payload.0.type" "conversation.message-timer-update"

isConvDescriptionUpdateNotif :: (HasCallStack, MakesValue n) => n -> App Bool
isConvDescriptionUpdateNotif n =
fieldEquals n "payload.0.type" "conversation.description-update"

isConvAccessUpdateNotif :: (HasCallStack, MakesValue n) => n -> App Bool
isConvAccessUpdateNotif n =
fieldEquals n "payload.0.type" "conversation.access-update"
Expand Down
65 changes: 65 additions & 0 deletions integration/test/Test/Conversation.hs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import Control.Concurrent (threadDelay)
import Control.Monad.Codensity
import Control.Monad.Reader
import qualified Data.Aeson as Aeson
import qualified Data.ByteString.Char8 as BS
import qualified Data.Text as T
import GHC.Stack
import MLS.Util
Expand Down Expand Up @@ -758,6 +759,70 @@ testConversationReceiptModeUpdate proto = do
resp.status `shouldMatchInt` 200
resp.json %. "receipt_mode" `shouldMatchInt` receiptMode

testConversationDescriptionUpdate :: (HasCallStack) => App ()
testConversationDescriptionUpdate = do
(owner, tid, [convMember, outsider]) <- createTeam OwnDomain 3
conv <-
postConversation
owner
(defProteus {team = Just tid, qualifiedUsers = [convMember], newUsersRole = "wire_member"})
>>= getJSON 201

let assertDescription :: (HasCallStack) => Response -> Int -> ByteString -> App ()
assertDescription resp version ciphertext = do
resp.status `shouldMatchInt` 200
resp.json %. "version" `shouldMatchInt` version
resp.json %. "ciphertext" `shouldMatchBase64` ciphertext

firstCiphertext = BS.pack "group description v1"
secondCiphertext = BS.pack "group description v2"

withWebSockets [owner, convMember] $ \[ownerWs, memberWs] -> do
bindResponse (updateConversationDescription owner conv (0 :: Int64) firstCiphertext) $ \resp ->
assertDescription resp 1 firstCiphertext

for_ [ownerWs, memberWs] $ \ws -> do
notif <- awaitMatch isConvDescriptionUpdateNotif ws
assertBool "notification should target the updated conversation" =<< isNotifConv conv notif
assertBool "notification should be emitted by the updating user" =<< isNotifFromUser owner notif
notif %. "payload.0.qualified_conversation" `shouldMatch` objQidObject conv
notif %. "payload.0.qualified_from" `shouldMatch` objQidObject owner
notif %. "payload.0.data.version" `shouldMatchInt` 1
notif %. "payload.0.data.ciphertext" `shouldMatchBase64` firstCiphertext

bindResponse (getConversationDescription owner conv) $ \resp ->
assertDescription resp 1 firstCiphertext

bindResponse (getConversationDescription convMember conv) $ \resp ->
assertDescription resp 1 firstCiphertext

bindResponse (updateConversationDescription owner conv (1 :: Int64) secondCiphertext) $ \resp ->
assertDescription resp 2 secondCiphertext

for_ [ownerWs, memberWs] $ \ws -> do
notif <- awaitMatch isConvDescriptionUpdateNotif ws
assertBool "notification should target the updated conversation" =<< isNotifConv conv notif
assertBool "notification should be emitted by the updating user" =<< isNotifFromUser owner notif
notif %. "payload.0.qualified_conversation" `shouldMatch` objQidObject conv
notif %. "payload.0.qualified_from" `shouldMatch` objQidObject owner
notif %. "payload.0.data.version" `shouldMatchInt` 2
notif %. "payload.0.data.ciphertext" `shouldMatchBase64` secondCiphertext

bindResponse (getConversationDescription owner conv) $ \resp ->
assertDescription resp 2 secondCiphertext

bindResponse (updateConversationDescription owner conv (0 :: Int64) (BS.pack "stale update")) $ \resp -> do
resp.status `shouldMatchInt` 403
resp.json %. "label" `shouldMatch` "invalid-op"

bindResponse (updateConversationDescription convMember conv (2 :: Int64) (BS.pack "non-admin update")) $ \resp -> do
resp.status `shouldMatchInt` 403
resp.json %. "label" `shouldMatch` "access-denied"

bindResponse (getConversationDescription outsider conv) $ \resp -> do
resp.status `shouldMatchInt` 403
resp.json %. "label" `shouldMatch` "access-denied"

testReceiptModeWithRemotesOk :: (HasCallStack) => App ()
testReceiptModeWithRemotesOk = do
[alice, bob] <- createAndConnectUsers [OwnDomain, OtherDomain]
Expand Down
43 changes: 43 additions & 0 deletions libs/wire-api/src/Wire/API/Conversation.hs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@ module Wire.API.Conversation
ExtraConversationData (..),
ConversationHistoryUpdate (..),

-- * conversation description
ConversationDescription (..),
ConversationDescriptionUpdate (..),

-- * re-exports
module Wire.API.Conversation.Member,
fromOwnConversation,
Expand All @@ -105,6 +109,7 @@ import Data.ByteString.Lazy qualified as LBS
import Data.Default
import Data.Domain
import Data.Id
import Data.Json.Util (Base64ByteString (..))
import Data.List.Extra (disjointOrd)
import Data.List.NonEmpty (NonEmpty)
import Data.Map qualified as Map
Expand Down Expand Up @@ -1338,6 +1343,44 @@ instance ToSchema ConversationHistoryUpdate where
ConversationHistoryUpdate
<$> (.history) .= field "history" schema

data ConversationDescription = ConversationDescription
{ descriptionVersion :: Int64,
descriptionCiphertext :: ByteString
}
deriving stock (Eq, Show, Generic)
deriving (Arbitrary) via (GenericUniform ConversationDescription)
deriving (FromJSON, ToJSON, S.ToSchema) via (Schema ConversationDescription)

instance ToSchema ConversationDescription where
schema =
object $
ConversationDescription
<$> descriptionVersion .= field "version" schema
<*> descriptionCiphertext
.= fieldWithDocModifier
"ciphertext"
(DS.description ?~ "Encrypted description payload, Base64 in JSON")
(Base64ByteString .= fmap fromBase64ByteString (unnamed schema))

data ConversationDescriptionUpdate = ConversationDescriptionUpdate
{ descriptionUpdateBaseVersion :: Int64,
descriptionUpdateCiphertext :: ByteString
}
deriving stock (Eq, Show, Generic)
deriving (Arbitrary) via (GenericUniform ConversationDescriptionUpdate)
deriving (FromJSON, ToJSON, S.ToSchema) via (Schema ConversationDescriptionUpdate)

instance ToSchema ConversationDescriptionUpdate where
schema =
object $
ConversationDescriptionUpdate
<$> descriptionUpdateBaseVersion .= field "base_version" schema
<*> descriptionUpdateCiphertext
.= fieldWithDocModifier
"ciphertext"
(DS.description ?~ "Encrypted description payload, Base64 in JSON")
(Base64ByteString .= fmap fromBase64ByteString (unnamed schema))

--------------------------------------------------------------------------------
-- MultiVerb instances

Expand Down
8 changes: 8 additions & 0 deletions libs/wire-api/src/Wire/API/Event/Conversation.hs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ module Wire.API.Event.Conversation
_EdConnect,
_EdConvReceiptModeUpdate,
_EdConvRename,
_EdConvDescriptionUpdate,
_EdConvDelete,
_EdConvAccessUpdate,
_EdConvMessageTimerUpdate,
Expand Down Expand Up @@ -175,6 +176,7 @@ data EventType
| MemberLeave
| MemberStateUpdate
| ConvRename
| ConvDescriptionUpdate
| ConvAccessUpdate
| ConvMessageTimerUpdate
| ConvCodeUpdate
Expand Down Expand Up @@ -203,6 +205,7 @@ instance ToSchema EventType where
element "conversation.member-leave" MemberLeave,
element "conversation.member-update" MemberStateUpdate,
element "conversation.rename" ConvRename,
element "conversation.description-update" ConvDescriptionUpdate,
element "conversation.access-update" ConvAccessUpdate,
element "conversation.receipt-mode-update" ConvReceiptModeUpdate,
element "conversation.message-timer-update" ConvMessageTimerUpdate,
Expand All @@ -227,6 +230,7 @@ data EventData
| EdConnect Connect
| EdConvReceiptModeUpdate ConversationReceiptModeUpdate
| EdConvRename ConversationRename
| EdConvDescriptionUpdate ConversationDescription
| EdConvDelete
| EdConvReset ConversationReset
| EdConvAccessUpdate ConversationAccessData
Expand All @@ -250,6 +254,7 @@ genEventData = \case
MemberLeave -> EdMembersLeave <$> arbitrary <*> arbitrary
MemberStateUpdate -> EdMemberUpdate <$> arbitrary
ConvRename -> EdConvRename <$> arbitrary
ConvDescriptionUpdate -> EdConvDescriptionUpdate <$> arbitrary
ConvAccessUpdate -> EdConvAccessUpdate <$> arbitrary
ConvMessageTimerUpdate -> EdConvMessageTimerUpdate <$> arbitrary
ConvCodeUpdate -> EdConvCodeUpdate <$> arbitrary
Expand All @@ -272,6 +277,7 @@ eventDataType (EdMembersJoin _) = MemberJoin
eventDataType (EdMembersLeave _ _) = MemberLeave
eventDataType (EdMemberUpdate _) = MemberStateUpdate
eventDataType (EdConvRename _) = ConvRename
eventDataType (EdConvDescriptionUpdate _) = ConvDescriptionUpdate
eventDataType (EdConvAccessUpdate _) = ConvAccessUpdate
eventDataType (EdConvMessageTimerUpdate _) = ConvMessageTimerUpdate
eventDataType (EdConvCodeUpdate _) = ConvCodeUpdate
Expand Down Expand Up @@ -312,6 +318,7 @@ isCellsConversationEvent eventType =
ProtocolUpdate -> False
AddPermissionUpdate -> False
ConvHistoryUpdate -> False
ConvDescriptionUpdate -> False

--------------------------------------------------------------------------------
-- Event data helpers
Expand Down Expand Up @@ -492,6 +499,7 @@ taggedEventDataSchema =
MemberLeave -> tag _EdMembersLeave (unnamed memberLeaveSchema)
MemberStateUpdate -> tag _EdMemberUpdate (unnamed schema)
ConvRename -> tag _EdConvRename (unnamed schema)
ConvDescriptionUpdate -> tag _EdConvDescriptionUpdate (unnamed schema)
-- FUTUREWORK: when V2 is dropped, it is fine to change this schema to
-- V3, since V3 clients are guaranteed to know how to parse V2 and V3
-- conversation access update events.
Expand Down
14 changes: 14 additions & 0 deletions libs/wire-api/src/Wire/API/PostgresMarshall.hs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import Data.Id
import Data.Json.Util (UTCTimeMillis (fromUTCTimeMillis), toUTCTimeMillis)
import Data.Misc
import Data.Profunctor
import Data.Range
import Data.Set qualified as Set
import Data.Text qualified as Text
import Data.Text.Ascii qualified as Ascii
Expand All @@ -45,6 +46,7 @@ import Data.Time (UTCTime)
import Data.UUID
import Data.Vector (Vector)
import Data.Vector qualified as V
import GHC.TypeLits
import Hasql.Statement
import Imports
import SAML2.WebSSO qualified as SAML
Expand Down Expand Up @@ -590,6 +592,12 @@ instance PostgresMarshall Int32 TeamInviteTag where
instance PostgresMarshall UUID SAML.IdPId where
postgresMarshall = SAML.fromIdPId

instance PostgresMarshall Text HttpsUrl where
postgresMarshall = httpsUrlToText

instance PostgresMarshall a (Range n m a) where
postgresMarshall = fromRange

---

class PostgresUnmarshall db domain where
Expand Down Expand Up @@ -1062,6 +1070,12 @@ instance PostgresUnmarshall Text Handle where
instance PostgresUnmarshall UTCTime UTCTimeMillis where
postgresUnmarshall = Right . toUTCTimeMillis

instance PostgresUnmarshall Text HttpsUrl where
postgresUnmarshall = mapLeft Text.pack . httpsUrlFromText

instance (KnownNat n, KnownNat m, Within a n m) => PostgresUnmarshall a (Range n m a) where
postgresUnmarshall = mapLeft Text.pack . checkedEither

---

lmapPG :: (PostgresMarshall db domain, Profunctor p) => p db x -> p domain x
Expand Down
8 changes: 8 additions & 0 deletions libs/wire-api/src/Wire/API/Routes/Public/Brig.hs
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,14 @@ type UserAPI =
:> QualifiedCaptureUserId "uid"
:> GetUserVerb
)
:<|> Named
"get-public-profile"
( Summary "Get a user's public profile"
:> "users"
:> "public"
:> Capture' '[Description "The user handle"] "handle" Handle
:> Get '[JSON] (Maybe PublicProfile)
)
:<|> Named
"update-user-email"
( Summary "Resend email address validation email."
Expand Down
27 changes: 27 additions & 0 deletions libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs
Original file line number Diff line number Diff line change
Expand Up @@ -1203,6 +1203,33 @@ type ConversationAPI =
(UpdateResponses "Name updated" "Name unchanged" Event)
(UpdateResult Event)
)
:<|> Named
"get-conversation-description"
( Summary "Get conversation description"
:> From 'V17
:> CanThrow 'ConvNotFound
:> CanThrow 'ConvAccessDenied
:> ZLocalUser
:> "conversations"
:> QualifiedCapture' '[Description "Conversation ID"] "cnv" ConvId
:> "description"
:> Get '[JSON] ConversationDescription
)
:<|> Named
"update-conversation-description"
( Summary "Update conversation description"
:> From 'V17
:> CanThrow 'ConvNotFound
:> CanThrow 'ConvAccessDenied
:> CanThrow 'InvalidOperation
:> ZLocalUser
:> ZConn
:> "conversations"
:> QualifiedCapture' '[Description "Conversation ID"] "cnv" ConvId
:> "description"
:> ReqBody '[JSON] ConversationDescriptionUpdate
:> Put '[JSON] ConversationDescription
)
-- This endpoint can lead to the following events being sent:
-- - ConvMessageTimerUpdate event to members
:<|> Named
Expand Down
Loading