From d432dfba2126ba004a85f0f79b47c3a39d06fbe7 Mon Sep 17 00:00:00 2001 From: JRoberts <8711996+jr-simplex@users.noreply.github.com> Date: Fri, 4 Nov 2022 12:00:03 +0400 Subject: [PATCH] core: include pending group link connections into chat previews on joining side (#1291) --- docs/protocol/simplex-chat.md | 2 +- docs/protocol/simplex-chat.schema.json | 6 ++++++ src/Simplex/Chat.hs | 4 ++-- src/Simplex/Chat/Call.hs | 6 +++--- src/Simplex/Chat/Messages.hs | 4 ++-- src/Simplex/Chat/Protocol.hs | 4 ++-- src/Simplex/Chat/Store.hs | 20 ++++++++++---------- src/Simplex/Chat/Types.hs | 13 +++++++------ 8 files changed, 33 insertions(+), 26 deletions(-) diff --git a/docs/protocol/simplex-chat.md b/docs/protocol/simplex-chat.md index f629f12bf8..0dd61d2921 100644 --- a/docs/protocol/simplex-chat.md +++ b/docs/protocol/simplex-chat.md @@ -199,7 +199,7 @@ Currently members can have one of three roles - `owner`, `admin` and `member`. T ### Messages to manage groups and add members -`x.grp.inv` message is sent to invite contact to the group via contact's direct connection and includes group member connection address. This message MUST only be sent by members with `admin` or `owner` role. +`x.grp.inv` message is sent to invite contact to the group via contact's direct connection and includes group member connection address. This message MUST only be sent by members with `admin` or `owner` role. Optional `groupLinkId` is included when this message is sent to contacts connected via the user's group link. This identifier is a random byte sequence, with no global or even local uniqueness - it is only used for the user's invitations to a given group to provide confirmation to the contact that the group invitation is for the same group the contact was connecting to via the group link, so that the invitation can be automatically accepted by the contact - the contact compares it with the group link id contained in the group link uri's data field. `x.grp.acpt` message is sent as part of group member connection handshake, only to the inviting user. diff --git a/docs/protocol/simplex-chat.schema.json b/docs/protocol/simplex-chat.schema.json index 020648a7a2..a9738190bd 100644 --- a/docs/protocol/simplex-chat.schema.json +++ b/docs/protocol/simplex-chat.schema.json @@ -108,6 +108,12 @@ "invitedMember": {"ref": "memberIdRole"}, "connRequest": {"ref": "connReqUri"}, "groupProfile": {"ref": "profile"} + }, + "optionalProperties": { + "groupLinkId": {"ref": "base64url"}, + "metadata": { + "comment": "used to identify invitation via group link" + } } }, "memberIdRole": { diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index b497765088..ae4174d32d 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -987,7 +987,7 @@ processChatCommand = \case when (memberStatus membership == GSMemInvited) $ throwChatError (CEGroupNotJoined gInfo) unless (memberActive membership) $ throwChatError CEGroupMemberNotActive groupLinkId <- GroupLinkId <$> (asks idsDrg >>= liftIO . (`randomBytes` 16)) - let crClientData = encodeJson $ CRGroupData groupLinkId + let crClientData = encodeJSON $ CRGroupData groupLinkId (connId, cReq) <- withAgent $ \a -> createConnection a True SCMContact $ Just crClientData withStore $ \db -> createGroupLink db user gInfo connId cReq groupLinkId pure $ CRGroupLinkCreated gInfo cReq @@ -1125,7 +1125,7 @@ processChatCommand = \case incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing let profileToSend = fromMaybe profile incognitoProfile connId <- withAgent $ \a -> joinConnection a True cReq $ directMessage (XContact profileToSend $ Just xContactId) - let groupLinkId = crClientData >>= decodeJson >>= \(CRGroupData gli) -> Just gli + let groupLinkId = crClientData >>= decodeJSON >>= \(CRGroupData gli) -> Just gli conn <- withStore' $ \db -> createConnReqConnection db userId connId cReqHash xContactId incognitoProfile groupLinkId toView $ CRNewContactConnection conn pure $ CRSentInvitation incognitoProfile diff --git a/src/Simplex/Chat/Call.hs b/src/Simplex/Chat/Call.hs index bdf0a05f77..8c4379d0ee 100644 --- a/src/Simplex/Chat/Call.hs +++ b/src/Simplex/Chat/Call.hs @@ -21,7 +21,7 @@ import Data.Time.Clock (UTCTime) import Database.SQLite.Simple.FromField (FromField (..)) import Database.SQLite.Simple.ToField (ToField (..)) import GHC.Generics (Generic) -import Simplex.Chat.Types (Contact, ContactId, decodeJson, encodeJson) +import Simplex.Chat.Types (Contact, ContactId, decodeJSON, encodeJSON) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (dropPrefix, enumJSON, fromTextField_, fstToLower, singleFieldJSON) @@ -100,10 +100,10 @@ instance ToJSON CallState where toEncoding = J.genericToEncoding $ singleFieldJSON fstToLower instance ToField CallState where - toField = toField . encodeJson + toField = toField . encodeJSON instance FromField CallState where - fromField = fromTextField_ decodeJson + fromField = fromTextField_ decodeJSON newtype CallId = CallId ByteString deriving (Eq, Show) diff --git a/src/Simplex/Chat/Messages.hs b/src/Simplex/Chat/Messages.hs index 1f91e1f127..39c5da4c83 100644 --- a/src/Simplex/Chat/Messages.hs +++ b/src/Simplex/Chat/Messages.hs @@ -726,7 +726,7 @@ msgDirToDeletedContent_ msgDir mode = case msgDir of -- platform independent instance ToField (CIContent d) where - toField = toField . encodeJson . dbJsonCIContent + toField = toField . encodeJSON . dbJsonCIContent -- platform specific instance ToJSON (CIContent d) where @@ -742,7 +742,7 @@ instance FromJSON ACIContent where parseJSON = fmap aciContentJSON . J.parseJSON -- platform independent -instance FromField ACIContent where fromField = fromTextField_ $ fmap aciContentDBJSON . decodeJson +instance FromField ACIContent where fromField = fromTextField_ $ fmap aciContentDBJSON . decodeJSON -- platform specific data JSONCIContent diff --git a/src/Simplex/Chat/Protocol.hs b/src/Simplex/Chat/Protocol.hs index bbe9b6f40c..5f5c24692a 100644 --- a/src/Simplex/Chat/Protocol.hs +++ b/src/Simplex/Chat/Protocol.hs @@ -391,10 +391,10 @@ instance ToJSON MsgContent where MCFile t -> J.pairs $ "type" .= MCFile_ <> "text" .= t instance ToField MsgContent where - toField = toField . encodeJson + toField = toField . encodeJSON instance FromField MsgContent where - fromField = fromTextField_ decodeJson + fromField = fromTextField_ decodeJSON data CMEventTag (e :: MsgEncoding) where XMsgNew_ :: CMEventTag 'Json diff --git a/src/Simplex/Chat/Store.hs b/src/Simplex/Chat/Store.hs index c99c94e101..7338794870 100644 --- a/src/Simplex/Chat/Store.hs +++ b/src/Simplex/Chat/Store.hs @@ -434,7 +434,7 @@ createConnReqConnection db userId acId cReqHash xContactId incognitoProfile grou |] ((userId, acId, pccConnStatus, ConnContact, cReqHash, xContactId) :. (customUserProfileId, isJust groupLinkId, groupLinkId, createdAt, createdAt)) pccConnId <- insertedRowId db - pure PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = True, viaUserContactLink = Nothing, customUserProfileId, connReqInv = Nothing, localAlias = "", createdAt, updatedAt = createdAt} + pure PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = True, viaUserContactLink = Nothing, groupLinkId, customUserProfileId, connReqInv = Nothing, localAlias = "", createdAt, updatedAt = createdAt} getConnReqContactXContactId :: DB.Connection -> UserId -> ConnReqUriHash -> IO (Maybe Contact, Maybe XContactId) getConnReqContactXContactId db userId cReqHash = do @@ -482,7 +482,7 @@ createDirectConnection db userId acId cReq pccConnStatus incognitoProfile = do |] (userId, acId, cReq, pccConnStatus, ConnContact, customUserProfileId, createdAt, createdAt) pccConnId <- insertedRowId db - pure PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = False, viaUserContactLink = Nothing, customUserProfileId, connReqInv = Just cReq, localAlias = "", createdAt, updatedAt = createdAt} + pure PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = False, viaUserContactLink = Nothing, groupLinkId = Nothing, customUserProfileId, connReqInv = Just cReq, localAlias = "", createdAt, updatedAt = createdAt} createIncognitoProfile_ :: DB.Connection -> UserId -> UTCTime -> Profile -> IO Int64 createIncognitoProfile_ db userId createdAt Profile {displayName, fullName, image} = do @@ -1203,7 +1203,7 @@ getPendingContactConnections db User {userId} = do <$> DB.queryNamed db [sql| - SELECT connection_id, agent_conn_id, conn_status, via_contact_uri_hash, via_user_contact_link, custom_user_profile_id, conn_req_inv, local_alias, created_at, updated_at + SELECT connection_id, agent_conn_id, conn_status, via_contact_uri_hash, via_user_contact_link, group_link_id, custom_user_profile_id, conn_req_inv, local_alias, created_at, updated_at FROM connections WHERE user_id = :user_id AND conn_type = :conn_type @@ -3342,13 +3342,13 @@ getContactConnectionChatPreviews_ db User {userId} _ = <$> DB.query db [sql| - SELECT connection_id, agent_conn_id, conn_status, via_contact_uri_hash, via_user_contact_link, custom_user_profile_id, conn_req_inv, local_alias, created_at, updated_at + SELECT connection_id, agent_conn_id, conn_status, via_contact_uri_hash, via_user_contact_link, group_link_id, custom_user_profile_id, conn_req_inv, local_alias, created_at, updated_at FROM connections - WHERE user_id = ? AND conn_type = ? AND contact_id IS NULL AND conn_level = 0 AND via_group_link = 0 AND via_contact IS NULL + WHERE user_id = ? AND conn_type = ? AND contact_id IS NULL AND conn_level = 0 AND via_contact IS NULL AND (via_group_link = 0 || (via_group_link = 1 AND group_link_id IS NOT NULL)) |] (userId, ConnContact) where - toContactConnectionChatPreview :: (Int64, ConnId, ConnStatus, Maybe ByteString, Maybe Int64, Maybe Int64, Maybe ConnReqInvitation, LocalAlias, UTCTime, UTCTime) -> AChat + toContactConnectionChatPreview :: (Int64, ConnId, ConnStatus, Maybe ByteString, Maybe Int64, Maybe GroupLinkId, Maybe Int64, Maybe ConnReqInvitation, LocalAlias, UTCTime, UTCTime) -> AChat toContactConnectionChatPreview connRow = let conn = toPendingContactConnection connRow stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False} @@ -3360,7 +3360,7 @@ getPendingContactConnection db userId connId = do DB.query db [sql| - SELECT connection_id, agent_conn_id, conn_status, via_contact_uri_hash, via_user_contact_link, custom_user_profile_id, conn_req_inv, local_alias, created_at, updated_at + SELECT connection_id, agent_conn_id, conn_status, via_contact_uri_hash, via_user_contact_link, group_link_id, custom_user_profile_id, conn_req_inv, local_alias, created_at, updated_at FROM connections WHERE user_id = ? AND connection_id = ? @@ -3394,9 +3394,9 @@ updateGroupSettings :: DB.Connection -> User -> Int64 -> ChatSettings -> IO () updateGroupSettings db User {userId} groupId ChatSettings {enableNtfs} = DB.execute db "UPDATE groups SET enable_ntfs = ? WHERE user_id = ? AND group_id = ?" (enableNtfs, userId, groupId) -toPendingContactConnection :: (Int64, ConnId, ConnStatus, Maybe ByteString, Maybe Int64, Maybe Int64, Maybe ConnReqInvitation, LocalAlias, UTCTime, UTCTime) -> PendingContactConnection -toPendingContactConnection (pccConnId, acId, pccConnStatus, connReqHash, viaUserContactLink, customUserProfileId, connReqInv, localAlias, createdAt, updatedAt) = - PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = isJust connReqHash, viaUserContactLink, customUserProfileId, connReqInv, localAlias, createdAt, updatedAt} +toPendingContactConnection :: (Int64, ConnId, ConnStatus, Maybe ByteString, Maybe Int64, Maybe GroupLinkId, Maybe Int64, Maybe ConnReqInvitation, LocalAlias, UTCTime, UTCTime) -> PendingContactConnection +toPendingContactConnection (pccConnId, acId, pccConnStatus, connReqHash, viaUserContactLink, groupLinkId, customUserProfileId, connReqInv, localAlias, createdAt, updatedAt) = + PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = isJust connReqHash, viaUserContactLink, groupLinkId, customUserProfileId, connReqInv, localAlias, createdAt, updatedAt} getDirectChat :: DB.Connection -> User -> Int64 -> ChatPagination -> Maybe String -> ExceptT StoreError IO (Chat 'CTDirect) getDirectChat db user contactId pagination search_ = do diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index 86bd6526d3..1fa06deb27 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -252,10 +252,10 @@ instance ToJSON ChatPreferences where toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True} instance ToField ChatPreferences where - toField = toField . encodeJson + toField = toField . encodeJSON instance FromField ChatPreferences where - fromField = fromTextField_ decodeJson + fromField = fromTextField_ decodeJSON data Preference = Preference {enable :: PrefSwitch} @@ -935,6 +935,7 @@ data PendingContactConnection = PendingContactConnection pccConnStatus :: ConnStatus, viaContactUri :: Bool, viaUserContactLink :: Maybe Int64, + groupLinkId :: Maybe GroupLinkId, customUserProfileId :: Maybe Int64, connReqInv :: Maybe ConnReqInvitation, localAlias :: Text, @@ -1164,8 +1165,8 @@ data XGrpMemIntroCont = XGrpMemIntroCont } deriving (Show) -encodeJson :: ToJSON a => a -> Text -encodeJson = safeDecodeUtf8 . LB.toStrict . J.encode +encodeJSON :: ToJSON a => a -> Text +encodeJSON = safeDecodeUtf8 . LB.toStrict . J.encode -decodeJson :: FromJSON a => Text -> Maybe a -decodeJson = J.decode . LB.fromStrict . encodeUtf8 +decodeJSON :: FromJSON a => Text -> Maybe a +decodeJSON = J.decode . LB.fromStrict . encodeUtf8