core: group link connection request uri data; automatically join groups over group links (#1275)

This commit is contained in:
JRoberts
2022-11-03 14:46:36 +04:00
committed by GitHub
parent 83599adc80
commit 5243613045
11 changed files with 198 additions and 180 deletions

View File

@@ -60,6 +60,7 @@ library
Simplex.Chat.Migrations.M20221021_auto_accept__group_links
Simplex.Chat.Migrations.M20221024_contact_used
Simplex.Chat.Migrations.M20221025_chat_settings
Simplex.Chat.Migrations.M20221029_group_link_id
Simplex.Chat.Mobile
Simplex.Chat.Options
Simplex.Chat.ProfileGenerator

View File

@@ -986,8 +986,10 @@ processChatCommand = \case
when (userRole < GRAdmin) $ throwChatError CEGroupUserRole
when (memberStatus membership == GSMemInvited) $ throwChatError (CEGroupNotJoined gInfo)
unless (memberActive membership) $ throwChatError CEGroupMemberNotActive
(connId, cReq) <- withAgent $ \a -> createConnection a True SCMContact Nothing
withStore $ \db -> createGroupLink db user gInfo connId cReq
groupLinkId <- GroupLinkId <$> (asks idsDrg >>= liftIO . (`randomBytes` 16))
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
APIDeleteGroupLink groupId -> withUser $ \user -> withChatLock "deleteGroupLink" $ do
gInfo <- withStore $ \db -> getGroupInfo db user groupId
@@ -1107,7 +1109,7 @@ processChatCommand = \case
CTGroup -> withStore $ \db -> getGroupChatItemIdByText db user cId (Just localDisplayName) (safeDecodeUtf8 msg)
_ -> throwChatError $ CECommandError "not supported"
connectViaContact :: UserId -> ConnectionRequestUri 'CMContact -> Profile -> m ChatResponse
connectViaContact userId cReq profile = withChatLock "connectViaContact" $ do
connectViaContact userId cReq@(CRContactUri ConnReqUriData {crClientData}) profile = withChatLock "connectViaContact" $ do
let cReqHash = ConnReqUriHash . C.sha256Hash $ strEncode cReq
withStore' (\db -> getConnReqContactXContactId db userId cReqHash) >>= \case
(Just contact, _) -> pure $ CRContactAlreadyExists contact
@@ -1123,7 +1125,8 @@ 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)
conn <- withStore' $ \db -> createConnReqConnection db userId connId cReqHash xContactId incognitoProfile
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
contactMember :: Contact -> [GroupMember] -> Maybe GroupMember
@@ -1211,7 +1214,7 @@ processChatCommand = \case
sendGrpInvitation :: User -> Contact -> GroupInfo -> GroupMember -> ConnReqInvitation -> m ()
sendGrpInvitation user ct@Contact {localDisplayName} GroupInfo {groupId, groupProfile, membership} GroupMember {groupMemberId, memberId, memberRole = memRole} cReq = do
let GroupMember {memberRole = userRole, memberId = userMemberId} = membership
groupInv = GroupInvitation (MemberIdRole userMemberId userRole) (MemberIdRole memberId memRole) cReq groupProfile
groupInv = GroupInvitation (MemberIdRole userMemberId userRole) (MemberIdRole memberId memRole) cReq groupProfile Nothing
(msg, _) <- sendDirectContactMessage ct $ XGrpInv groupInv
let content = CISndGroupInvitation (CIGroupInvitation {groupId, groupMemberId, localDisplayName, groupProfile, status = CIGISPending}) memRole
ci <- saveSndChatItem user (CDDirectSnd ct) msg content Nothing Nothing
@@ -1744,7 +1747,7 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage =
withStore $ \db -> createNewContactMemberAsync db gVar user groupId ct GRMember groupConnIds
probeMatchingContacts ct $ contactConnIncognito ct
_ -> pure ()
Just (gInfo@GroupInfo {membership}, m@GroupMember {activeConn}) -> do
Just (gInfo@GroupInfo {membership}, m@GroupMember {activeConn}) ->
when (maybe False ((== ConnReady) . connStatus) activeConn) $ do
notifyMemberConnected gInfo m
let connectedIncognito = contactConnIncognito ct || memberIncognito membership
@@ -1797,13 +1800,14 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage =
Nothing -> messageError "implementation error: invitee does not have contact"
Just ct -> do
withStore' $ \db -> setNewContactMemberConnRequest db user m cReq
sendGrpInvitation ct m
groupLinkId <- withStore' $ \db -> getGroupLinkId db user gInfo
sendGrpInvitation ct m groupLinkId
toView $ CRSentGroupInvitation gInfo ct m
where
sendGrpInvitation :: Contact -> GroupMember -> m ()
sendGrpInvitation ct GroupMember {memberId, memberRole = memRole} = do
sendGrpInvitation :: Contact -> GroupMember -> Maybe GroupLinkId -> m ()
sendGrpInvitation ct GroupMember {memberId, memberRole = memRole} groupLinkId = do
let GroupMember {memberRole = userRole, memberId = userMemberId} = membership
groupInv = GroupInvitation (MemberIdRole userMemberId userRole) (MemberIdRole memberId memRole) cReq groupProfile
groupInv = GroupInvitation (MemberIdRole userMemberId userRole) (MemberIdRole memberId memRole) cReq groupProfile groupLinkId
(_msg, _) <- sendDirectContactMessage ct $ XGrpInv groupInv
-- we could link chat item with sent group invitation message (_msg)
createInternalChatItem (CDGroupRcv gInfo m) (CIRcvGroupEvent RGEInvitedViaGroupLink) Nothing
@@ -2416,18 +2420,32 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage =
toView . CRNewChatItem $ AChatItem SCTGroup SMDRcv (GroupChat gInfo) ci
processGroupInvitation :: Contact -> GroupInvitation -> RcvMessage -> MsgMeta -> m ()
processGroupInvitation ct@Contact {localDisplayName = c, activeConn = Connection {customUserProfileId}} inv@GroupInvitation {fromMember = (MemberIdRole fromMemId fromRole), invitedMember = (MemberIdRole memId memRole)} msg msgMeta = do
processGroupInvitation ct@Contact {localDisplayName = c, activeConn = Connection {connId, customUserProfileId}} inv@GroupInvitation {fromMember = (MemberIdRole fromMemId fromRole), invitedMember = (MemberIdRole memId memRole), connRequest, groupLinkId} msg msgMeta = do
checkIntegrityCreateItem (CDDirectRcv ct) msgMeta
when (fromRole < GRMember || fromRole < memRole) $ throwChatError (CEGroupContactRole c)
when (fromMemId == memId) $ throwChatError CEGroupDuplicateMemberId
-- [incognito] if direct connection with host is incognito, create membership using the same incognito profile
gInfo@GroupInfo {groupId, localDisplayName, groupProfile, membership = GroupMember {groupMemberId}} <- withStore $ \db -> createGroupInvitation db user ct inv customUserProfileId
let content = CIRcvGroupInvitation (CIGroupInvitation {groupId, groupMemberId, localDisplayName, groupProfile, status = CIGISPending}) memRole
ci <- saveRcvChatItem user (CDDirectRcv ct) msg msgMeta content Nothing
withStore' $ \db -> setGroupInvitationChatItemId db user groupId (chatItemId' ci)
toView . CRNewChatItem $ AChatItem SCTDirect SMDRcv (DirectChat ct) ci
toView $ CRReceivedGroupInvitation gInfo ct memRole
showToast ("#" <> localDisplayName <> " " <> c <> "> ") "invited you to join the group"
(gInfo@GroupInfo {groupId, localDisplayName, groupProfile, membership = membership@GroupMember {groupMemberId, memberId}}, hostId) <- withStore $ \db -> createGroupInvitation db user ct inv customUserProfileId
groupLinkId' <- withStore' $ \db -> getConnectionGroupLinkId db user connId
if sameGroupLinkId groupLinkId groupLinkId'
then do
connIds <- joinAgentConnectionAsync user True connRequest . directMessage $ XGrpAcpt memberId
withStore' $ \db -> do
createMemberConnectionAsync db user hostId connIds
updateGroupMemberStatusById db userId hostId GSMemAccepted
updateGroupMemberStatus db userId membership GSMemAccepted
toView $ CRUserAcceptedGroupSent gInfo {membership = membership {memberStatus = GSMemAccepted}}
else do
let content = CIRcvGroupInvitation (CIGroupInvitation {groupId, groupMemberId, localDisplayName, groupProfile, status = CIGISPending}) memRole
ci <- saveRcvChatItem user (CDDirectRcv ct) msg msgMeta content Nothing
withStore' $ \db -> setGroupInvitationChatItemId db user groupId (chatItemId' ci)
toView . CRNewChatItem $ AChatItem SCTDirect SMDRcv (DirectChat ct) ci
toView $ CRReceivedGroupInvitation gInfo ct memRole
showToast ("#" <> localDisplayName <> " " <> c <> "> ") "invited you to join the group"
where
sameGroupLinkId :: Maybe GroupLinkId -> Maybe GroupLinkId -> Bool
sameGroupLinkId (Just gli) (Just gli') = gli == gli'
sameGroupLinkId _ _ = False
checkIntegrityCreateItem :: forall c. ChatTypeI c => ChatDirection c 'MDRcv -> MsgMeta -> m ()
checkIntegrityCreateItem cd MsgMeta {integrity, broker = (_, brokerTs)} = case integrity of

View File

@@ -15,16 +15,13 @@ import Data.Aeson (FromJSON, ToJSON)
import qualified Data.Aeson as J
import qualified Data.Attoparsec.ByteString.Char8 as A
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Lazy.Char8 as LB
import Data.Int (Int64)
import Data.Text (Text)
import Data.Text.Encoding (encodeUtf8)
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)
import Simplex.Messaging.Util (safeDecodeUtf8)
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)
@@ -103,10 +100,10 @@ instance ToJSON CallState where
toEncoding = J.genericToEncoding $ singleFieldJSON fstToLower
instance ToField CallState where
toField = toField . safeDecodeUtf8 . LB.toStrict . J.encode
toField = toField . encodeJson
instance FromField CallState where
fromField = fromTextField_ $ J.decode . LB.fromStrict . encodeUtf8
fromField = fromTextField_ decodeJson
newtype CallId = CallId ByteString
deriving (Eq, Show)

View File

@@ -726,7 +726,7 @@ msgDirToDeletedContent_ msgDir mode = case msgDir of
-- platform independent
instance ToField (CIContent d) where
toField = toField . safeDecodeUtf8 . LB.toStrict . J.encode . 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 . J.decode . LB.fromStrict . encodeUtf8
instance FromField ACIContent where fromField = fromTextField_ $ fmap aciContentDBJSON . decodeJson
-- platform specific
data JSONCIContent

View File

@@ -0,0 +1,14 @@
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.Migrations.M20221029_group_link_id where
import Database.SQLite.Simple (Query)
import Database.SQLite.Simple.QQ (sql)
m20221029_group_link_id :: Query
m20221029_group_link_id =
[sql|
ALTER TABLE user_contact_links ADD COLUMN group_link_id BLOB;
ALTER TABLE connections ADD COLUMN group_link_id BLOB;
|]

View File

@@ -257,6 +257,7 @@ CREATE TABLE connections(
conn_req_inv BLOB,
local_alias DEFAULT '' CHECK(local_alias NOT NULL),
via_group_link INTEGER DEFAULT 0 CHECK(via_group_link NOT NULL),
group_link_id BLOB,
FOREIGN KEY(snd_file_id, connection_id)
REFERENCES snd_files(file_id, connection_id)
ON DELETE CASCADE
@@ -273,6 +274,7 @@ CREATE TABLE user_contact_links(
auto_reply_msg_content TEXT DEFAULT NULL,
group_id INTEGER REFERENCES groups ON DELETE CASCADE,
auto_accept_incognito INTEGER DEFAULT 0 CHECK(auto_accept_incognito NOT NULL),
group_link_id BLOB,
UNIQUE(user_id, local_display_name)
);
CREATE TABLE contact_requests(

View File

@@ -391,10 +391,10 @@ instance ToJSON MsgContent where
MCFile t -> J.pairs $ "type" .= MCFile_ <> "text" .= t
instance ToField MsgContent where
toField = toField . safeDecodeUtf8 . LB.toStrict . J.encode
toField = toField . encodeJson
instance FromField MsgContent where
fromField = fromTextField_ $ J.decode . LB.fromStrict . encodeUtf8
fromField = fromTextField_ decodeJson
data CMEventTag (e :: MsgEncoding) where
XMsgNew_ :: CMEventTag 'Json

View File

@@ -58,6 +58,8 @@ module Simplex.Chat.Store
getGroupLinkConnection,
deleteGroupLink,
getGroupLink,
getGroupLinkId,
getConnectionGroupLinkId,
createOrUpdateContactRequest,
getContactRequest,
getContactRequestIdByName,
@@ -97,6 +99,7 @@ module Simplex.Chat.Store
setNewContactMemberConnRequest,
getMemberInvitation,
createMemberConnection,
createMemberConnectionAsync,
updateGroupMemberStatus,
updateGroupMemberStatusById,
createNewGroupMember,
@@ -244,7 +247,7 @@ import Data.Either (rights)
import Data.Function (on)
import Data.Functor (($>))
import Data.Int (Int64)
import Data.List (find, sortBy, sortOn)
import Data.List (sortBy, sortOn)
import Data.List.NonEmpty (NonEmpty)
import Data.Maybe (fromMaybe, isJust, isNothing, listToMaybe)
import Data.Ord (Down (..))
@@ -292,6 +295,7 @@ import Simplex.Chat.Migrations.M20221019_unread_chat
import Simplex.Chat.Migrations.M20221021_auto_accept__group_links
import Simplex.Chat.Migrations.M20221024_contact_used
import Simplex.Chat.Migrations.M20221025_chat_settings
import Simplex.Chat.Migrations.M20221029_group_link_id
import Simplex.Chat.Protocol
import Simplex.Chat.Types
import Simplex.Messaging.Agent.Protocol (ACorrId, AgentMsgId, ConnId, InvitationId, MsgMeta (..))
@@ -337,7 +341,8 @@ schemaMigrations =
("20221019_unread_chat", m20221019_unread_chat),
("20221021_auto_accept__group_links", m20221021_auto_accept__group_links),
("20221024_contact_used", m20221024_contact_used),
("20221025_chat_settings", m20221025_chat_settings)
("20221025_chat_settings", m20221025_chat_settings),
("20221029_group_link_id", m20221029_group_link_id)
]
-- | The list of migrations in ascending order by date
@@ -414,8 +419,8 @@ setActiveUser db userId = do
DB.execute_ db "UPDATE users SET active_user = 0"
DB.execute db "UPDATE users SET active_user = 1 WHERE user_id = ?" (Only userId)
createConnReqConnection :: DB.Connection -> UserId -> ConnId -> ConnReqUriHash -> XContactId -> Maybe Profile -> IO PendingContactConnection
createConnReqConnection db userId acId cReqHash xContactId incognitoProfile = do
createConnReqConnection :: DB.Connection -> UserId -> ConnId -> ConnReqUriHash -> XContactId -> Maybe Profile -> Maybe GroupLinkId -> IO PendingContactConnection
createConnReqConnection db userId acId cReqHash xContactId incognitoProfile groupLinkId = do
createdAt <- getCurrentTime
customUserProfileId <- mapM (createIncognitoProfile_ db userId createdAt) incognitoProfile
let pccConnStatus = ConnJoined
@@ -424,10 +429,10 @@ createConnReqConnection db userId acId cReqHash xContactId incognitoProfile = do
[sql|
INSERT INTO connections (
user_id, agent_conn_id, conn_status, conn_type,
via_contact_uri_hash, xcontact_id, custom_user_profile_id, created_at, updated_at
) VALUES (?,?,?,?,?,?,?,?,?)
via_contact_uri_hash, xcontact_id, custom_user_profile_id, via_group_link, group_link_id, created_at, updated_at
) VALUES (?,?,?,?,?,?,?,?,?,?,?)
|]
(userId, acId, pccConnStatus, ConnContact, cReqHash, xContactId, customUserProfileId, createdAt, createdAt)
((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}
@@ -895,16 +900,16 @@ updateUserAddressAutoAccept db userId autoAccept = do
Just AutoAccept {acceptIncognito, autoReply} -> (True, acceptIncognito, autoReply)
_ -> (False, False, Nothing)
createGroupLink :: DB.Connection -> User -> GroupInfo -> ConnId -> ConnReqContact -> ExceptT StoreError IO ()
createGroupLink db User {userId} groupInfo@GroupInfo {groupId, localDisplayName} agentConnId cReq =
createGroupLink :: DB.Connection -> User -> GroupInfo -> ConnId -> ConnReqContact -> GroupLinkId -> ExceptT StoreError IO ()
createGroupLink db User {userId} groupInfo@GroupInfo {groupId, localDisplayName} agentConnId cReq groupLinkId =
checkConstraint (SEDuplicateGroupLink groupInfo) . liftIO $ do
currentTs <- getCurrentTime
DB.execute
db
"INSERT INTO user_contact_links (user_id, group_id, local_display_name, conn_req_contact, auto_accept, created_at, updated_at) VALUES (?,?,?,?,?,?,?)"
(userId, groupId, "group_link_" <> localDisplayName, cReq, True, currentTs, currentTs)
groupLinkId <- insertedRowId db
void $ createConnection_ db userId ConnUserContact (Just groupLinkId) agentConnId Nothing Nothing Nothing 0 currentTs
"INSERT INTO user_contact_links (user_id, group_id, group_link_id, local_display_name, conn_req_contact, auto_accept, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)"
(userId, groupId, groupLinkId, "group_link_" <> localDisplayName, cReq, True, currentTs, currentTs)
userContactLinkId <- insertedRowId db
void $ createConnection_ db userId ConnUserContact (Just userContactLinkId) agentConnId Nothing Nothing Nothing 0 currentTs
getGroupLinkConnection :: DB.Connection -> User -> GroupInfo -> ExceptT StoreError IO Connection
getGroupLinkConnection db User {userId} groupInfo@GroupInfo {groupId} =
@@ -963,7 +968,17 @@ deleteGroupLink db User {userId} GroupInfo {groupId} = do
getGroupLink :: DB.Connection -> User -> GroupInfo -> ExceptT StoreError IO ConnReqContact
getGroupLink db User {userId} gInfo@GroupInfo {groupId} =
ExceptT . firstRow fromOnly (SEGroupLinkNotFound gInfo) $
DB.query db "SELECT conn_req_contact FROM user_contact_links WHERE user_id = ? AND group_id = ?" (userId, groupId)
DB.query db "SELECT conn_req_contact FROM user_contact_links WHERE user_id = ? AND group_id = ? LIMIT 1" (userId, groupId)
getGroupLinkId :: DB.Connection -> User -> GroupInfo -> IO (Maybe GroupLinkId)
getGroupLinkId db User {userId} GroupInfo {groupId} =
fmap join . maybeFirstRow fromOnly $
DB.query db "SELECT group_link_id FROM user_contact_links WHERE user_id = ? AND group_id = ? LIMIT 1" (userId, groupId)
getConnectionGroupLinkId :: DB.Connection -> User -> Int64 -> IO (Maybe GroupLinkId)
getConnectionGroupLinkId db User {userId} connId =
fmap join . maybeFirstRow fromOnly $
DB.query db "SELECT group_link_id FROM connections WHERE user_id = ? AND connection_id = ? LIMIT 1" (userId, connId)
createOrUpdateContactRequest :: DB.Connection -> UserId -> Int64 -> InvitationId -> Profile -> Maybe XContactId -> ExceptT StoreError IO ContactOrRequest
createOrUpdateContactRequest db userId userContactLinkId invId Profile {displayName, fullName, image, preferences} xContactId_ =
@@ -1597,25 +1612,28 @@ createNewGroup db gVar user@User {userId} groupProfile = ExceptT $ do
pure GroupInfo {groupId, localDisplayName = ldn, groupProfile, membership, hostConnCustomUserProfileId = Nothing, chatSettings, createdAt = currentTs, updatedAt = currentTs}
-- | creates a new group record for the group the current user was invited to, or returns an existing one
createGroupInvitation :: DB.Connection -> User -> Contact -> GroupInvitation -> Maybe ProfileId -> ExceptT StoreError IO GroupInfo
createGroupInvitation :: DB.Connection -> User -> Contact -> GroupInvitation -> Maybe ProfileId -> ExceptT StoreError IO (GroupInfo, GroupMemberId)
createGroupInvitation db user@User {userId} contact@Contact {contactId, activeConn = Connection {customUserProfileId}} GroupInvitation {fromMember, invitedMember, connRequest, groupProfile} incognitoProfileId = do
liftIO getInvitationGroupId_ >>= \case
Nothing -> createGroupInvitation_
Just gId -> do
gInfo@GroupInfo {membership, groupProfile = p'} <- getGroupInfo db user gId
hostId <- getHostMemberId_ db user gId
let GroupMember {groupMemberId, memberId, memberRole} = membership
MemberIdRole {memberId = memberId', memberRole = memberRole'} = invitedMember
liftIO . when (memberId /= memberId' || memberRole /= memberRole') $
DB.execute db "UPDATE group_members SET member_id = ?, member_role = ? WHERE group_member_id = ?" (memberId', memberRole', groupMemberId)
if p' == groupProfile
then pure gInfo
else updateGroupProfile db user gInfo groupProfile
gInfo' <-
if p' == groupProfile
then pure gInfo
else updateGroupProfile db user gInfo groupProfile
pure (gInfo', hostId)
where
getInvitationGroupId_ :: IO (Maybe Int64)
getInvitationGroupId_ =
maybeFirstRow fromOnly $
DB.query db "SELECT group_id FROM groups WHERE inv_queue_info = ? AND user_id = ? LIMIT 1" (connRequest, userId)
createGroupInvitation_ :: ExceptT StoreError IO GroupInfo
createGroupInvitation_ :: ExceptT StoreError IO (GroupInfo, GroupMemberId)
createGroupInvitation_ = do
let GroupProfile {displayName, fullName, image, preferences} = groupProfile
ExceptT $
@@ -1632,10 +1650,15 @@ createGroupInvitation db user@User {userId} contact@Contact {contactId, activeCo
"INSERT INTO groups (group_profile_id, local_display_name, inv_queue_info, host_conn_custom_user_profile_id, user_id, enable_ntfs, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)"
(profileId, localDisplayName, connRequest, customUserProfileId, userId, True, currentTs, currentTs)
insertedRowId db
_ <- createContactMemberInv_ db user groupId contact fromMember GCHostMember GSMemInvited IBUnknown Nothing currentTs
GroupMember {groupMemberId} <- createContactMemberInv_ db user groupId contact fromMember GCHostMember GSMemInvited IBUnknown Nothing currentTs
membership <- createContactMemberInv_ db user groupId user invitedMember GCUserMember GSMemInvited (IBContact contactId) incognitoProfileId currentTs
let chatSettings = ChatSettings {enableNtfs = True}
pure GroupInfo {groupId, localDisplayName, groupProfile, membership, hostConnCustomUserProfileId = customUserProfileId, chatSettings, createdAt = currentTs, updatedAt = currentTs}
pure (GroupInfo {groupId, localDisplayName, groupProfile, membership, hostConnCustomUserProfileId = customUserProfileId, chatSettings, createdAt = currentTs, updatedAt = currentTs}, groupMemberId)
getHostMemberId_ :: DB.Connection -> User -> GroupId -> ExceptT StoreError IO GroupMemberId
getHostMemberId_ db User {userId} groupId =
ExceptT . firstRow fromOnly (SEHostMemberIdNotFound groupId) $
DB.query db "SELECT group_member_id FROM group_members WHERE user_id = ? AND group_id = ? AND member_category = ?" (userId, groupId, GCHostMember)
createContactMemberInv_ :: IsContact a => DB.Connection -> User -> GroupId -> a -> MemberIdRole -> GroupMemberCategory -> GroupMemberStatus -> InvitedBy -> Maybe ProfileId -> UTCTime -> ExceptT StoreError IO GroupMember
createContactMemberInv_ db User {userId, userContactId} groupId userOrContact MemberIdRole {memberId, memberRole} memberCategory memberStatus invitedBy incognitoProfileId createdAt = do
@@ -1845,15 +1868,14 @@ toContactMember :: User -> (GroupMemberRow :. MaybeConnectionRow) -> GroupMember
toContactMember User {userContactId} (memberRow :. connRow) =
(toGroupMember userContactId memberRow) {activeConn = toMaybeConnection connRow}
-- TODO no need to load all members to find the member who invited the user,
-- instead of findFromContact there could be a query
getGroupInvitation :: DB.Connection -> User -> GroupId -> ExceptT StoreError IO ReceivedGroupInvitation
getGroupInvitation db user groupId = do
cReq <- getConnRec_ user
Group groupInfo@GroupInfo {membership} members <- getGroup db user groupId
when (memberStatus membership /= GSMemInvited) $ throwError SEGroupAlreadyJoined
case (cReq, findFromContact (invitedBy membership) members) of
(Just connRequest, Just fromMember) ->
getGroupInvitation db user groupId =
getConnRec_ user >>= \case
Just connRequest -> do
groupInfo@GroupInfo {membership} <- getGroupInfo db user groupId
when (memberStatus membership /= GSMemInvited) $ throwError SEGroupAlreadyJoined
hostId <- getHostMemberId_ db user groupId
fromMember <- getGroupMember db user groupId hostId
pure ReceivedGroupInvitation {fromMember, connRequest, groupInfo}
_ -> throwError SEGroupInvitationNotFound
where
@@ -1861,9 +1883,6 @@ getGroupInvitation db user groupId = do
getConnRec_ User {userId} = ExceptT $ do
firstRow fromOnly (SEGroupNotFound groupId) $
DB.query db "SELECT g.inv_queue_info FROM groups g WHERE g.group_id = ? AND g.user_id = ?" (groupId, userId)
findFromContact :: InvitedBy -> [GroupMember] -> Maybe GroupMember
findFromContact (IBContact contactId) = find ((== Just contactId) . memberContactId)
findFromContact _ = const Nothing
type GroupMemberRow = ((Int64, Int64, MemberId, GroupMemberRole, GroupMemberCategory, GroupMemberStatus) :. (Maybe Int64, ContactName, Maybe ContactId, ProfileId, ProfileId, ContactName, Text, Maybe ImageData, LocalAlias, Maybe ChatPreferences))
@@ -1982,6 +2001,12 @@ createMemberConnection db userId GroupMember {groupMemberId} agentConnId = do
currentTs <- getCurrentTime
void $ createMemberConnection_ db userId groupMemberId agentConnId Nothing 0 currentTs
createMemberConnectionAsync :: DB.Connection -> User -> GroupMemberId -> (CommandId, ConnId) -> IO ()
createMemberConnectionAsync db user@User {userId} groupMemberId (cmdId, agentConnId) = do
currentTs <- getCurrentTime
Connection {connId} <- createMemberConnection_ db userId groupMemberId agentConnId Nothing 0 currentTs
setCommandConnId db user cmdId connId
updateGroupMemberStatus :: DB.Connection -> UserId -> GroupMember -> GroupMemberStatus -> IO ()
updateGroupMemberStatus db userId GroupMember {groupMemberId} = updateGroupMemberStatusById db userId groupMemberId
@@ -4559,6 +4584,7 @@ data StoreError
| SEProfileNotFound {profileId :: Int64}
| SEDuplicateGroupLink {groupInfo :: GroupInfo}
| SEGroupLinkNotFound {groupInfo :: GroupInfo}
| SEHostMemberIdNotFound {groupId :: Int64}
deriving (Show, Exception, Generic)
instance ToJSON StoreError where

View File

@@ -42,8 +42,8 @@ import Database.SQLite.Simple.ToField (ToField (..))
import GHC.Generics (Generic)
import Simplex.Messaging.Agent.Protocol (ACommandTag (..), ACorrId, AParty (..), ConnId, ConnectionMode (..), ConnectionRequestUri, InvitationId)
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (dropPrefix, fromTextField_, sumTypeJSON)
import Simplex.Messaging.Util ((<$?>), safeDecodeUtf8)
import Simplex.Messaging.Parsers (dropPrefix, fromTextField_, sumTypeJSON, taggedObjectJSON)
import Simplex.Messaging.Util (safeDecodeUtf8, (<$?>))
class IsContact a where
contactId' :: a -> ContactId
@@ -252,10 +252,10 @@ instance ToJSON ChatPreferences where
toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True}
instance ToField ChatPreferences where
toField = toField . safeDecodeUtf8 . LB.toStrict . J.encode
toField = toField . encodeJson
instance FromField ChatPreferences where
fromField = fromTextField_ $ J.decode . LB.fromStrict . encodeUtf8
fromField = fromTextField_ decodeJson
data Preference = Preference
{enable :: PrefSwitch}
@@ -360,11 +360,41 @@ instance ToField ImageData where toField (ImageData t) = toField t
instance FromField ImageData where fromField = fmap ImageData . fromField
data CReqClientData = CRGroupData {groupLinkId :: GroupLinkId}
deriving (Generic)
instance ToJSON CReqClientData where
toJSON = J.genericToJSON . taggedObjectJSON $ dropPrefix "CD"
toEncoding = J.genericToEncoding . taggedObjectJSON $ dropPrefix "CD"
instance FromJSON CReqClientData where
parseJSON = J.genericParseJSON . taggedObjectJSON $ dropPrefix "CD"
newtype GroupLinkId = GroupLinkId {unGroupLinkId :: ByteString} -- used to identify invitation via group link
deriving (Eq, Show)
instance FromField GroupLinkId where fromField f = GroupLinkId <$> fromField f
instance ToField GroupLinkId where toField (GroupLinkId g) = toField g
instance StrEncoding GroupLinkId where
strEncode (GroupLinkId g) = strEncode g
strDecode s = GroupLinkId <$> strDecode s
strP = GroupLinkId <$> strP
instance FromJSON GroupLinkId where
parseJSON = strParseJSON "GroupLinkId"
instance ToJSON GroupLinkId where
toJSON = strToJSON
toEncoding = strToJEncoding
data GroupInvitation = GroupInvitation
{ fromMember :: MemberIdRole,
invitedMember :: MemberIdRole,
connRequest :: ConnReqInvitation,
groupProfile :: GroupProfile
groupProfile :: GroupProfile,
groupLinkId :: Maybe GroupLinkId
}
deriving (Eq, Show, Generic, FromJSON)
@@ -1133,3 +1163,9 @@ data XGrpMemIntroCont = XGrpMemIntroCont
groupConnReq :: ConnReqInvitation
}
deriving (Show)
encodeJson :: ToJSON a => a -> Text
encodeJson = safeDecodeUtf8 . LB.toStrict . J.encode
decodeJson :: FromJSON a => Text -> Maybe a
decodeJson = J.decode . LB.fromStrict . encodeUtf8

View File

@@ -107,7 +107,7 @@ chatTests = do
it "accept contact request incognito" testAcceptContactRequestIncognito
it "join group incognito" testJoinGroupIncognito
it "can't invite contact to whom user connected incognito to a group" testCantInviteContactIncognito
it "can't see global preferences update" testCantSeeGlobalPrefsUpdateIncognito
it "can't see global preferences update" testCantSeeGlobalPrefsUpdateIncognito
describe "contact aliases and prefs" $ do
it "set contact alias" testSetAlias
it "set connection alias" testSetConnectionAlias
@@ -142,7 +142,6 @@ chatTests = do
it "create group link, join via group link" testGroupLink
it "sending message to contact created via group link marks it used" testGroupLinkContactUsed
it "create group link, join via group link - incognito membership" testGroupLinkIncognitoMembership
it "deleting invited member does not leave broken chat item" testGroupLinkDeleteInvitedMemberNoBrokenItem
describe "queue rotation" $ do
it "switch contact to a different queue" testSwitchContact
it "switch group member to a different queue" testSwitchGroupMember
@@ -2693,7 +2692,6 @@ testCantInviteContactIncognito = testChat2 aliceProfile bobProfile $
-- bob doesn't receive invitation
(bob </)
testCantSeeGlobalPrefsUpdateIncognito :: IO ()
testCantSeeGlobalPrefsUpdateIncognito = testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
@@ -2733,7 +2731,7 @@ testCantSeeGlobalPrefsUpdateIncognito = testChat3 aliceProfile bobProfile cathPr
threadDelay 1000000
cath ##> "/_set prefs @2 {}"
cath <## "preferences were updated: contact's voice messages are on, user's voice messages are unset"
testSetAlias :: IO ()
testSetAlias = testChat2 aliceProfile bobProfile $
\alice bob -> do
@@ -3403,23 +3401,21 @@ testGroupLink =
concurrentlyN_
[ do
alice <## "bob (Bob): contact is connected"
alice <## "bob invited to group #team via your group link",
alice <## "bob invited to group #team via your group link"
alice <## "#team: bob joined the group",
do
bob <## "alice (Alice): contact is connected"
bob <## "#team: alice invites you to join the group as member"
bob <## "use /j team to accept"
bob <## "#team: you joined the group"
]
alice #$> ("/_get chat #1 count=100", chat, [(0, "invited via your group link")])
alice @@@ [("#team", "invited via your group link")] -- contacts connected via group link are not in chat previews
alice #$> ("/_get chat #1 count=100", chat, [(0, "invited via your group link"), (0, "connected")])
-- contacts connected via group link are not in chat previews
alice @@@ [("#team", "connected")]
bob @@@ [("#team", "connected")]
-- calling /_get chat api marks it as used and adds it to chat previews
alice #$> ("/_get chat @2 count=100", chat, [])
alice @@@ [("@bob", ""), ("#team", "invited via your group link")]
alice @@@ [("@bob", ""), ("#team", "connected")]
alice <##> bob
alice @@@ [("@bob", "hey"), ("#team", "invited via your group link")]
bob ##> "/j team"
concurrently_
(alice <## "#team: bob joined the group")
(bob <## "#team: you joined the group")
alice @@@ [("@bob", "hey"), ("#team", "connected")]
-- user address doesn't interfere
alice ##> "/ad"
@@ -3443,18 +3439,12 @@ testGroupLink =
alice <## "cath_1 (Catherine): contact is connected"
alice <## "cath_1 invited to group #team via your group link"
alice <## "contact cath_1 is merged into cath"
alice <## "use @cath <message> to send messages",
alice <## "use @cath <message> to send messages"
alice <## "#team: cath joined the group",
do
cath <## "alice_1 (Alice): contact is connected"
cath <## "contact alice_1 is merged into alice"
cath <## "use @alice <message> to send messages"
cath <## "#team: alice invites you to join the group as member"
cath <## "use /j team to accept"
]
cath ##> "/j team"
concurrentlyN_
[ alice <## "#team: cath joined the group",
do
cath <## "#team: you joined the group"
cath <## "#team: member bob (Bob) is connected",
do
@@ -3507,18 +3497,21 @@ testGroupLinkContactUsed =
concurrentlyN_
[ do
alice <## "bob (Bob): contact is connected"
alice <## "bob invited to group #team via your group link",
alice <## "bob invited to group #team via your group link"
alice <## "#team: bob joined the group",
do
bob <## "alice (Alice): contact is connected"
bob <## "#team: alice invites you to join the group as member"
bob <## "use /j team to accept"
bob <## "#team: you joined the group"
]
-- sending message to contact marks it as used
alice @@@ [("#team", "invited via your group link")]
-- sending/receiving a message marks contact as used
alice @@@ [("#team", "connected")]
bob @@@ [("#team", "connected")]
alice #> "@bob hello"
bob <# "alice> hello"
alice #$> ("/clear bob", id, "bob: all messages are removed locally ONLY")
alice @@@ [("@bob", ""), ("#team", "invited via your group link")]
alice @@@ [("@bob", ""), ("#team", "connected")]
bob #$> ("/clear alice", id, "alice: all messages are removed locally ONLY")
bob @@@ [("@alice", ""), ("#team", "connected")]
testGroupLinkIncognitoMembership :: IO ()
testGroupLinkIncognitoMembership =
@@ -3565,26 +3558,20 @@ testGroupLinkIncognitoMembership =
[ do
bob <## ("cath (Catherine): contact is connected, your incognito profile for this contact is " <> bobIncognito)
bob <## "use /info cath to print out this incognito profile again"
bob <## "cath invited to group #team via your group link",
bob <## "cath invited to group #team via your group link"
bob <## "#team: cath joined the group",
do
cath <## (bobIncognito <> ": contact is connected")
cath <## ("#team: " <> bobIncognito <> " invites you to join the group as member")
cath <## "use /j team to accept"
]
bob ?#> "@cath hi, I'm incognito"
cath <# (bobIncognito <> "> hi, I'm incognito")
cath #> ("@" <> bobIncognito <> " hey, I'm cath")
bob ?<# "cath> hey, I'm cath"
cath ##> "/j team"
concurrentlyN_
[ bob <## "#team: cath joined the group",
do
cath <## "#team: you joined the group"
cath <## "#team: member alice (Alice) is connected",
do
alice <## ("#team: " <> bobIncognito <> " added cath (Catherine) to the group (connecting...)")
alice <## "#team: new member cath is connected"
]
bob ?#> "@cath hi, I'm incognito"
cath <# (bobIncognito <> "> hi, I'm incognito")
cath #> ("@" <> bobIncognito <> " hey, I'm cath")
bob ?<# "cath> hey, I'm cath"
-- dan joins incognito
dan #$> ("/incognito on", id, "ok")
dan ##> ("/c " <> gLink)
@@ -3597,22 +3584,11 @@ testGroupLinkIncognitoMembership =
[ do
bob <## (danIncognito <> ": contact is connected, your incognito profile for this contact is " <> bobIncognito)
bob <## ("use /info " <> danIncognito <> " to print out this incognito profile again")
bob <## (danIncognito <> " invited to group #team via your group link"),
bob <## (danIncognito <> " invited to group #team via your group link")
bob <## ("#team: " <> danIncognito <> " joined the group"),
do
dan <## (bobIncognito <> ": contact is connected, your incognito profile for this contact is " <> danIncognito)
dan <## ("use /info " <> bobIncognito <> " to print out this incognito profile again")
dan <## ("#team: " <> bobIncognito <> " invites you to join the group as member")
dan <## ("use /j team to join incognito as " <> danIncognito)
]
dan #$> ("/incognito off", id, "ok")
bob ?#> ("@" <> danIncognito <> " hi, I'm incognito")
dan ?<# (bobIncognito <> "> hi, I'm incognito")
dan ?#> ("@" <> bobIncognito <> " hey, me too")
bob ?<# (danIncognito <> "> hey, me too")
dan ##> "/j team"
concurrentlyN_
[ bob <## ("#team: " <> danIncognito <> " joined the group"),
do
dan <## ("#team: you joined the group incognito as " <> danIncognito)
dan
<### [ "#team: member alice (Alice) is connected",
@@ -3625,6 +3601,11 @@ testGroupLinkIncognitoMembership =
cath <## ("#team: " <> bobIncognito <> " added " <> danIncognito <> " to the group (connecting...)")
cath <## ("#team: new member " <> danIncognito <> " is connected")
]
dan #$> ("/incognito off", id, "ok")
bob ?#> ("@" <> danIncognito <> " hi, I'm incognito")
dan ?<# (bobIncognito <> "> hi, I'm incognito")
dan ?#> ("@" <> bobIncognito <> " hey, me too")
bob ?<# (danIncognito <> "> hey, me too")
alice #> "#team hello"
concurrentlyN_
[ bob ?<# "#team alice> hello",
@@ -3650,66 +3631,6 @@ testGroupLinkIncognitoMembership =
cath <# ("#team " <> danIncognito <> "> how is it going?")
]
testGroupLinkDeleteInvitedMemberNoBrokenItem :: IO ()
testGroupLinkDeleteInvitedMemberNoBrokenItem =
testChat2 aliceProfile bobProfile $
\alice bob -> do
alice ##> "/g team"
alice <## "group #team is created"
alice <## "use /a team <name> to add members"
alice ##> "/create link #team"
gLink <- getGroupLink alice "team" True
bob ##> ("/c " <> gLink)
bob <## "connection request sent!"
alice <## "bob (Bob): accepting request to join group #team..."
concurrentlyN_
[ do
alice <## "bob (Bob): contact is connected"
alice <## "bob invited to group #team via your group link",
do
bob <## "alice (Alice): contact is connected"
bob <## "#team: alice invites you to join the group as member"
bob <## "use /j team to accept"
]
alice #$> ("/_get chat #1 count=100", chat, [(0, "invited via your group link")])
alice @@@ [("#team", "invited via your group link")]
-- removing invited member who connected via group link does not leave broken chat item
alice ##> "/rm team bob"
alice <## "#team: you removed bob from the group"
alice #$> ("/_get chat #1 count=100", chat, [])
alice @@@ [("#team", "")]
-- removing member deletes unused group contact
alice ##> "@bob hi"
alice <## "no contact bob"
(bob </)
bob ##> "/j team"
bob <## "error: connection authorization failed - this could happen if connection was deleted, secured with different credentials, or due to a bug - please re-create the connection"
-- repeat request is prohibited because of the re-used XContactId, until contact is deleted
bob ##> ("/c " <> gLink)
bob <## "alice (Alice): contact already exists"
bob ##> "/d alice"
bob <## "alice: contact is deleted"
bob ##> ("/c " <> gLink)
bob <## "connection request sent!"
alice <## "bob (Bob): accepting request to join group #team..."
concurrentlyN_
[ do
alice <## "bob (Bob): contact is connected"
alice <## "bob invited to group #team via your group link",
do
bob <## "alice_1 (Alice): contact is connected"
bob <## "#team_1 (team): alice_1 invites you to join the group as member"
bob <## "use /j team_1 to accept"
]
bob ##> "/j team_1"
concurrently_
(alice <## "#team: bob joined the group")
(bob <## "#team_1: you joined the group")
alice #> "#team hello"
bob <# "#team_1 alice_1> hello"
bob #> "#team_1 hi there"
alice <# "#team bob> hi there"
testSwitchContact :: IO ()
testSwitchContact =
testChat2 aliceProfile bobProfile $

View File

@@ -196,7 +196,10 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do
==# XContact testProfile Nothing
it "x.grp.inv" $
"{\"event\":\"x.grp.inv\",\"params\":{\"groupInvitation\":{\"connRequest\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"invitedMember\":{\"memberRole\":\"member\",\"memberId\":\"BQYHCA==\"},\"groupProfile\":{\"fullName\":\"Team\",\"displayName\":\"team\",\"preferences\":{\"voice\":{\"enable\":\"on\"}}},\"fromMember\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\"}}}}"
#==# XGrpInv GroupInvitation {fromMember = MemberIdRole (MemberId "\1\2\3\4") GRAdmin, invitedMember = MemberIdRole (MemberId "\5\6\7\8") GRMember, connRequest = testConnReq, groupProfile = testGroupProfile}
#==# XGrpInv GroupInvitation {fromMember = MemberIdRole (MemberId "\1\2\3\4") GRAdmin, invitedMember = MemberIdRole (MemberId "\5\6\7\8") GRMember, connRequest = testConnReq, groupProfile = testGroupProfile, groupLinkId = Nothing}
it "x.grp.inv with group link id" $
"{\"event\":\"x.grp.inv\",\"params\":{\"groupInvitation\":{\"connRequest\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"invitedMember\":{\"memberRole\":\"member\",\"memberId\":\"BQYHCA==\"},\"groupProfile\":{\"fullName\":\"Team\",\"displayName\":\"team\",\"preferences\":{\"voice\":{\"enable\":\"on\"}}},\"fromMember\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\"}, \"groupLinkId\":\"AQIDBA==\"}}}"
#==# XGrpInv GroupInvitation {fromMember = MemberIdRole (MemberId "\1\2\3\4") GRAdmin, invitedMember = MemberIdRole (MemberId "\5\6\7\8") GRMember, connRequest = testConnReq, groupProfile = testGroupProfile, groupLinkId = Just $ GroupLinkId "\1\2\3\4"}
it "x.grp.acpt without incognito profile" $
"{\"event\":\"x.grp.acpt\",\"params\":{\"memberId\":\"AQIDBA==\"}}"
#==# XGrpAcpt (MemberId "\1\2\3\4")