merge profiles using contact probe (#86)

* chat commands to list members and to quit chat

* merge profiles using probe

* merge contacts connected to the same user based on successful profile probe

* delete display name after merging contacts

* probe: rename "existing" contacts to "matching"
This commit is contained in:
Evgeny Poberezkin 2021-07-27 08:08:05 +01:00 committed by GitHub
parent cc4cb78209
commit b3af93e0ad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 279 additions and 29 deletions

View File

@ -5,6 +5,8 @@ CREATE TABLE contact_profiles ( -- remote user profile
properties TEXT NOT NULL DEFAULT '{}' -- JSON with contact profile properties properties TEXT NOT NULL DEFAULT '{}' -- JSON with contact profile properties
); );
CREATE INDEX contact_profiles_index ON contact_profiles (display_name, full_name);
CREATE TABLE users ( CREATE TABLE users (
user_id INTEGER PRIMARY KEY, user_id INTEGER PRIMARY KEY,
contact_id INTEGER NOT NULL UNIQUE REFERENCES contacts ON DELETE CASCADE contact_id INTEGER NOT NULL UNIQUE REFERENCES contacts ON DELETE CASCADE
@ -43,6 +45,30 @@ CREATE TABLE contacts (
UNIQUE (user_id, contact_profile_id) UNIQUE (user_id, contact_profile_id)
); );
CREATE TABLE sent_probes (
sent_probe_id INTEGER PRIMARY KEY,
contact_id INTEGER NOT NULL UNIQUE REFERENCES contacts ON DELETE CASCADE,
probe BLOB NOT NULL,
user_id INTEGER NOT NULL REFERENCES users,
UNIQUE (user_id, probe)
);
CREATE TABLE sent_probe_hashes (
sent_probe_hash_id INTEGER PRIMARY KEY,
sent_probe_id INTEGER NOT NULL REFERENCES sent_probes ON DELETE CASCADE,
contact_id INTEGER NOT NULL REFERENCES contacts ON DELETE CASCADE,
user_id INTEGER NOT NULL REFERENCES users,
UNIQUE (sent_probe_id, contact_id)
);
CREATE TABLE received_probes (
received_probe_id INTEGER PRIMARY KEY,
contact_id INTEGER NOT NULL REFERENCES contacts ON DELETE CASCADE,
probe BLOB,
probe_hash BLOB NOT NULL,
user_id INTEGER NOT NULL REFERENCES users
);
CREATE TABLE known_servers( CREATE TABLE known_servers(
server_id INTEGER PRIMARY KEY, server_id INTEGER PRIMARY KEY,
host TEXT NOT NULL, host TEXT NOT NULL,

View File

@ -43,9 +43,10 @@ import Simplex.Messaging.Agent
import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..)) import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..))
import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.Protocol
import Simplex.Messaging.Client (smpDefaultConfig) import Simplex.Messaging.Client (smpDefaultConfig)
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Parsers (parseAll) import Simplex.Messaging.Parsers (parseAll)
import Simplex.Messaging.Util (raceAny_) import Simplex.Messaging.Util (raceAny_)
import System.Exit (exitFailure) import System.Exit (exitFailure, exitSuccess)
import System.IO (hFlush, stdout) import System.IO (hFlush, stdout)
import Text.Read (readMaybe) import Text.Read (readMaybe)
import UnliftIO.Async (race_) import UnliftIO.Async (race_)
@ -67,6 +68,7 @@ data ChatCommand
| DeleteGroup GroupName | DeleteGroup GroupName
| ListMembers GroupName | ListMembers GroupName
| SendGroupMessage GroupName ByteString | SendGroupMessage GroupName ByteString
| QuitChat
deriving (Show) deriving (Show)
cfg :: AgentConfig cfg :: AgentConfig
@ -186,7 +188,9 @@ processChatCommand user@User {userId, profile} = \case
RemoveMember _gName _cName -> pure () RemoveMember _gName _cName -> pure ()
LeaveGroup _gName -> pure () LeaveGroup _gName -> pure ()
DeleteGroup _gName -> pure () DeleteGroup _gName -> pure ()
ListMembers _gName -> pure () ListMembers gName -> do
group <- withStore $ \st -> getGroup st user gName
showGroupMembers group
SendGroupMessage gName msg -> do SendGroupMessage gName msg -> do
-- TODO save sent messages -- TODO save sent messages
-- TODO save pending message delivery for members without connections -- TODO save pending message delivery for members without connections
@ -194,6 +198,7 @@ processChatCommand user@User {userId, profile} = \case
let msgEvent = XMsgNew $ MsgContent MTText [] [MsgContentBody {contentType = SimplexContentType XCText, contentData = msg}] let msgEvent = XMsgNew $ MsgContent MTText [] [MsgContentBody {contentType = SimplexContentType XCText, contentData = msg}]
sendGroupMessage members msgEvent sendGroupMessage members msgEvent
setActive $ ActiveG gName setActive $ ActiveG gName
QuitChat -> liftIO exitSuccess
where where
isMember :: Contact -> [GroupMember] -> Bool isMember :: Contact -> [GroupMember] -> Bool
isMember Contact {contactId} members = isJust $ find ((== Just contactId) . memberContactId) members isMember Contact {contactId} members = isJust $ find ((== Just contactId) . memberContactId) members
@ -272,6 +277,9 @@ processAgentMessage user@User {userId, profile} agentConnId agentMessage = do
XMsgNew (MsgContent MTText [] body) -> newTextMessage c meta $ find (isSimplexContentType XCText) body XMsgNew (MsgContent MTText [] body) -> newTextMessage c meta $ find (isSimplexContentType XCText) body
XInfo _ -> pure () -- TODO profile update XInfo _ -> pure () -- TODO profile update
XGrpInv gInv -> processGroupInvitation ct gInv XGrpInv gInv -> processGroupInvitation ct gInv
XInfoProbe probe -> xInfoProbe ct probe
XInfoProbeCheck probeHash -> xInfoProbeCheck ct probeHash
XInfoProbeOk probe -> xInfoProbeOk ct probe
_ -> pure () _ -> pure ()
CONF confId connInfo -> do CONF confId connInfo -> do
-- confirming direct connection with a member -- confirming direct connection with a member
@ -298,7 +306,9 @@ processAgentMessage user@User {userId, profile} agentConnId agentMessage = do
setActive $ ActiveC c setActive $ ActiveC c
showToast (c <> "> ") "connected" showToast (c <> "> ") "connected"
Just (gName, m) -> Just (gName, m) ->
when (memberIsReady m) $ notifyMemberConnected gName m when (memberIsReady m) $ do
notifyMemberConnected gName m
when (memberCategory m == GCPreMember) $ probeMatchingContacts ct
END -> do END -> do
showContactDisconnected c showContactDisconnected c
showToast (c <> "> ") "disconnected" showToast (c <> "> ") "disconnected"
@ -342,6 +352,7 @@ processAgentMessage user@User {userId, profile} agentConnId agentMessage = do
group@Group {members, membership} <- withStore $ \st -> getGroup st user gName group@Group {members, membership} <- withStore $ \st -> getGroup st user gName
withStore $ \st -> do withStore $ \st -> do
updateGroupMemberStatus st userId (groupMemberId m) GSMemConnected updateGroupMemberStatus st userId (groupMemberId m) GSMemConnected
unless (memberActive membership) $
updateGroupMemberStatus st userId (groupMemberId membership) GSMemConnected updateGroupMemberStatus st userId (groupMemberId membership) GSMemConnected
-- TODO forward any pending (GMIntroInvReceived) introductions -- TODO forward any pending (GMIntroInvReceived) introductions
case memberCategory m of case memberCategory m of
@ -366,7 +377,9 @@ processAgentMessage user@User {userId, profile} agentConnId agentMessage = do
notifyMemberConnected gName m notifyMemberConnected gName m
messageError "implementation error: connected member does not have contact" messageError "implementation error: connected member does not have contact"
Just ct -> Just ct ->
when (contactIsReady ct) $ notifyMemberConnected gName m when (contactIsReady ct) $ do
notifyMemberConnected gName m
when (memberCategory m == GCPreMember) $ probeMatchingContacts ct
MSG meta msgBody -> do MSG meta msgBody -> do
ChatMessage {chatMsgEvent} <- liftEither $ parseChatMessage msgBody ChatMessage {chatMsgEvent} <- liftEither $ parseChatMessage msgBody
case chatMsgEvent of case chatMsgEvent of
@ -385,6 +398,19 @@ processAgentMessage user@User {userId, profile} agentConnId agentMessage = do
setActive $ ActiveG gName setActive $ ActiveG gName
showToast ("#" <> gName) $ "member " <> localDisplayName <> " is connected" showToast ("#" <> gName) $ "member " <> localDisplayName <> " is connected"
probeMatchingContacts :: Contact -> m ()
probeMatchingContacts ct = do
gVar <- asks idsDrg
(probe, probeId) <- withStore $ \st -> createSentProbe st gVar userId ct
sendDirectMessage (contactConnId ct) $ XInfoProbe probe
cs <- withStore (\st -> getMatchingContacts st userId ct)
let probeHash = C.sha256Hash probe
forM_ cs $ \c -> sendProbeHash c probeHash probeId `catchError` const (pure ())
where
sendProbeHash c probeHash probeId = do
sendDirectMessage (contactConnId c) $ XInfoProbeCheck probeHash
withStore $ \st -> createSentProbeHash st userId probeId c
messageWarning :: Text -> m () messageWarning :: Text -> m ()
messageWarning = liftIO . print messageWarning = liftIO . print
@ -416,6 +442,32 @@ processAgentMessage user@User {userId, profile} agentConnId agentMessage = do
group <- withStore $ \st -> createGroupInvitation st user ct inv group <- withStore $ \st -> createGroupInvitation st user ct inv
showReceivedGroupInvitation group localDisplayName memRole showReceivedGroupInvitation group localDisplayName memRole
xInfoProbe :: Contact -> ByteString -> m ()
xInfoProbe c2 probe = do
r <- withStore $ \st -> matchReceivedProbe st userId c2 probe
forM_ r $ \c1 -> probeMatch c1 c2 probe
xInfoProbeCheck :: Contact -> ByteString -> m ()
xInfoProbeCheck c1 probeHash = do
r <- withStore $ \st -> matchReceivedProbeHash st userId c1 probeHash
forM_ r $ \(c2, probe) -> probeMatch c1 c2 probe
probeMatch :: Contact -> Contact -> ByteString -> m ()
probeMatch c1@Contact {profile = p1} c2@Contact {profile = p2} probe =
when (p1 == p2) $ do
sendDirectMessage (contactConnId c1) $ XInfoProbeOk probe
mergeContacts c1 c2
xInfoProbeOk :: Contact -> ByteString -> m ()
xInfoProbeOk c1 probe = do
r <- withStore $ \st -> matchSentProbe st userId c1 probe
forM_ r $ \c2 -> mergeContacts c1 c2
mergeContacts :: Contact -> Contact -> m ()
mergeContacts to from = do
withStore $ \st -> mergeContactRecords st userId to from
showContactsMerged to from
parseChatMessage :: ByteString -> Either ChatError ChatMessage parseChatMessage :: ByteString -> Either ChatError ChatMessage
parseChatMessage msgBody = first ChatErrorMessage (parseAll rawChatMessageP msgBody >>= toChatMessage) parseChatMessage msgBody = first ChatErrorMessage (parseAll rawChatMessageP msgBody >>= toChatMessage)
@ -597,13 +649,14 @@ chatCommandP =
<|> ("/join #" <|> "/join " <|> "/j #" <|> "/j ") *> (JoinGroup <$> displayName) <|> ("/join #" <|> "/join " <|> "/j #" <|> "/j ") *> (JoinGroup <$> displayName)
<|> ("/remove #" <|> "/rm #") *> (RemoveMember <$> displayName <* A.space <*> displayName) <|> ("/remove #" <|> "/rm #") *> (RemoveMember <$> displayName <* A.space <*> displayName)
<|> ("/delete #" <|> "/d #") *> (DeleteGroup <$> displayName) <|> ("/delete #" <|> "/d #") *> (DeleteGroup <$> displayName)
<|> ("/members #" <|> "/ms #") *> (ListMembers <$> displayName) <|> ("/members #" <|> "/members " <|> "/ms #" <|> "/ms ") *> (ListMembers <$> displayName)
<|> A.char '#' *> (SendGroupMessage <$> displayName <* A.space <*> A.takeByteString) <|> A.char '#' *> (SendGroupMessage <$> displayName <* A.space <*> A.takeByteString)
<|> ("/add" <|> "/a") $> AddContact <|> ("/add" <|> "/a") $> AddContact
<|> ("/connect " <|> "/c ") *> (Connect <$> smpQueueInfoP) <|> ("/connect " <|> "/c ") *> (Connect <$> smpQueueInfoP)
<|> ("/delete @" <|> "/delete " <|> "/d @" <|> "/d ") *> (DeleteContact <$> displayName) <|> ("/delete @" <|> "/delete " <|> "/d @" <|> "/d ") *> (DeleteContact <$> displayName)
<|> A.char '@' *> (SendMessage <$> displayName <*> (A.space *> A.takeByteString)) <|> A.char '@' *> (SendMessage <$> displayName <*> (A.space *> A.takeByteString))
<|> ("/markdown" <|> "/m") $> MarkdownHelp <|> ("/markdown" <|> "/m") $> MarkdownHelp
<|> ("/quit" <|> "/q") $> QuitChat
where where
displayName = safeDecodeUtf8 <$> (B.cons <$> A.satisfy refChar <*> A.takeTill (== ' ')) displayName = safeDecodeUtf8 <$> (B.cons <$> A.satisfy refChar <*> A.takeTill (== ' '))
refChar c = c > ' ' && c /= '#' && c /= '@' refChar c = c > ' ' && c /= '#' && c /= '@'

View File

@ -56,8 +56,8 @@ data ChatMsgEvent
| XGrpMemCon MemberId | XGrpMemCon MemberId
| XGrpMemConAll MemberId | XGrpMemConAll MemberId
| XInfoProbe ByteString | XInfoProbe ByteString
| XInfoProbeCheck MemberId ByteString | XInfoProbeCheck ByteString
| XInfoProbeOk MemberId ByteString | XInfoProbeOk ByteString
| XOk | XOk
deriving (Eq, Show) deriving (Eq, Show)
@ -124,10 +124,10 @@ toChatMessage RawChatMessage {chatMsgId, chatMsgEvent, chatMsgParams, chatMsgBod
chatMsg . XGrpMemConAll =<< B64.decode memId chatMsg . XGrpMemConAll =<< B64.decode memId
("x.info.probe", [probe]) -> do ("x.info.probe", [probe]) -> do
chatMsg . XInfoProbe =<< B64.decode probe chatMsg . XInfoProbe =<< B64.decode probe
("x.info.probe.check", [memId, probeHash]) -> do ("x.info.probe.check", [probeHash]) -> do
chatMsg =<< (XInfoProbeCheck <$> B64.decode memId <*> B64.decode probeHash) chatMsg =<< (XInfoProbeCheck <$> B64.decode probeHash)
("x.info.probe.ok", [memId, probe]) -> do ("x.info.probe.ok", [probe]) -> do
chatMsg =<< (XInfoProbeOk <$> B64.decode memId <*> B64.decode probe) chatMsg =<< (XInfoProbeOk <$> B64.decode probe)
("x.ok", []) -> ("x.ok", []) ->
chatMsg XOk chatMsg XOk
_ -> Left $ "bad syntax or unsupported event " <> B.unpack chatMsgEvent _ -> Left $ "bad syntax or unsupported event " <> B.unpack chatMsgEvent
@ -202,10 +202,10 @@ rawChatMessage ChatMessage {chatMsgId, chatMsgEvent, chatDAG} =
rawMsg "x.grp.mem.con.all" [B64.encode memId] [] rawMsg "x.grp.mem.con.all" [B64.encode memId] []
XInfoProbe probe -> XInfoProbe probe ->
rawMsg "x.info.probe" [B64.encode probe] [] rawMsg "x.info.probe" [B64.encode probe] []
XInfoProbeCheck memId probeHash -> XInfoProbeCheck probeHash ->
rawMsg "x.info.probe.check" [B64.encode memId, B64.encode probeHash] [] rawMsg "x.info.probe.check" [B64.encode probeHash] []
XInfoProbeOk memId probe -> XInfoProbeOk probe ->
rawMsg "x.info.probe.ok" [B64.encode memId, B64.encode probe] [] rawMsg "x.info.probe.ok" [B64.encode probe] []
XOk -> XOk ->
rawMsg "x.ok" [] [] rawMsg "x.ok" [] []
where where

View File

@ -44,6 +44,14 @@ module Simplex.Chat.Store
saveMemberInvitation, saveMemberInvitation,
getViaGroupMember, getViaGroupMember,
getViaGroupContact, getViaGroupContact,
getMatchingContacts,
randomBytes,
createSentProbe,
createSentProbeHash,
matchReceivedProbe,
matchReceivedProbeHash,
matchSentProbe,
mergeContactRecords,
) )
where where
@ -73,6 +81,7 @@ import Simplex.Chat.Types
import Simplex.Messaging.Agent.Protocol (AParty (..), ConnId, SMPQueueInfo) import Simplex.Messaging.Agent.Protocol (AParty (..), ConnId, SMPQueueInfo)
import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore (..), createSQLiteStore, withTransaction) import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore (..), createSQLiteStore, withTransaction)
import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..)) import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..))
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Util (bshow, liftIOEither) import Simplex.Messaging.Util (bshow, liftIOEither)
import System.FilePath (takeBaseName, takeExtension) import System.FilePath (takeBaseName, takeExtension)
import UnliftIO.STM import UnliftIO.STM
@ -293,6 +302,126 @@ toMaybeConnection (Just connId, Just agentConnId, Just connLevel, viaContact, Ju
Just $ toConnection (connId, agentConnId, connLevel, viaContact, connStatus, connType, contactId, groupMemberId, createdAt) Just $ toConnection (connId, agentConnId, connLevel, viaContact, connStatus, connType, contactId, groupMemberId, createdAt)
toMaybeConnection _ = Nothing toMaybeConnection _ = Nothing
getMatchingContacts :: MonadUnliftIO m => SQLiteStore -> UserId -> Contact -> m [Contact]
getMatchingContacts st userId Contact {contactId, profile = Profile {displayName, fullName}} =
liftIO . withTransaction st $ \db -> do
contactNames <-
map fromOnly
<$> DB.queryNamed
db
[sql|
SELECT ct.local_display_name
FROM contacts ct
JOIN contact_profiles p ON ct.contact_profile_id = p.contact_profile_id
WHERE ct.user_id = :user_id AND ct.contact_id != :contact_id
AND p.display_name = :display_name AND p.full_name = :full_name
|]
[ ":user_id" := userId,
":contact_id" := contactId,
":display_name" := displayName,
":full_name" := fullName
]
rights <$> mapM (runExceptT . getContact_ db userId) contactNames
createSentProbe :: StoreMonad m => SQLiteStore -> TVar ChaChaDRG -> UserId -> Contact -> m (ByteString, Int64)
createSentProbe st gVar userId _to@Contact {contactId} =
liftIOEither . withTransaction st $ \db ->
createWithRandomBytes 32 gVar $ \probe -> do
DB.execute db "INSERT INTO sent_probes (contact_id, probe, user_id) VALUES (?,?,?)" (contactId, probe, userId)
(probe,) <$> insertedRowId db
createSentProbeHash :: MonadUnliftIO m => SQLiteStore -> UserId -> Int64 -> Contact -> m ()
createSentProbeHash st userId probeId _to@Contact {contactId} =
liftIO . withTransaction st $ \db ->
DB.execute db "INSERT INTO sent_probe_hashes (sent_probe_id, contact_id, user_id) VALUES (?,?,?)" (probeId, contactId, userId)
matchReceivedProbe :: MonadUnliftIO m => SQLiteStore -> UserId -> Contact -> ByteString -> m (Maybe Contact)
matchReceivedProbe st userId _from@Contact {contactId} probe =
liftIO . withTransaction st $ \db -> do
let probeHash = C.sha256Hash probe
contactNames <-
map fromOnly
<$> DB.query
db
[sql|
SELECT c.local_display_name
FROM contacts c
JOIN received_probes r ON r.contact_id = c.contact_id
WHERE c.user_id = ? AND r.probe_hash = ? AND r.probe IS NULL
|]
(userId, probeHash)
DB.execute db "INSERT INTO received_probes (contact_id, probe, probe_hash, user_id) VALUES (?,?,?,?)" (contactId, probe, probeHash, userId)
case contactNames of
[] -> pure Nothing
cName : _ ->
either (const Nothing) Just
<$> runExceptT (getContact_ db userId cName)
matchReceivedProbeHash :: MonadUnliftIO m => SQLiteStore -> UserId -> Contact -> ByteString -> m (Maybe (Contact, ByteString))
matchReceivedProbeHash st userId _from@Contact {contactId} probeHash =
liftIO . withTransaction st $ \db -> do
namesAndProbes <-
DB.query
db
[sql|
SELECT c.local_display_name, r.probe
FROM contacts c
JOIN received_probes r ON r.contact_id = c.contact_id
WHERE c.user_id = ? AND r.probe_hash = ? AND r.probe IS NOT NULL
|]
(userId, probeHash)
DB.execute db "INSERT INTO received_probes (contact_id, probe_hash, user_id) VALUES (?,?,?)" (contactId, probeHash, userId)
case namesAndProbes of
[] -> pure Nothing
(cName, probe) : _ ->
either (const Nothing) (Just . (,probe))
<$> runExceptT (getContact_ db userId cName)
matchSentProbe :: MonadUnliftIO m => SQLiteStore -> UserId -> Contact -> ByteString -> m (Maybe Contact)
matchSentProbe st userId _from@Contact {contactId} probe =
liftIO . withTransaction st $ \db -> do
contactNames <-
map fromOnly
<$> DB.query
db
[sql|
SELECT c.local_display_name
FROM contacts c
JOIN sent_probes s ON s.contact_id = c.contact_id
JOIN sent_probe_hashes h ON h.sent_probe_id = s.sent_probe_id
WHERE c.user_id = ? AND s.probe = ? AND h.contact_id = ?
|]
(userId, probe, contactId)
case contactNames of
[] -> pure Nothing
cName : _ ->
either (const Nothing) Just
<$> runExceptT (getContact_ db userId cName)
mergeContactRecords :: MonadUnliftIO m => SQLiteStore -> UserId -> Contact -> Contact -> m ()
mergeContactRecords st userId Contact {contactId = toContactId} Contact {contactId = fromContactId, localDisplayName} =
liftIO . withTransaction st $ \db -> do
DB.execute db "UPDATE connections SET contact_id = ? WHERE contact_id = ? AND user_id = ?" (toContactId, fromContactId, userId)
DB.execute db "UPDATE connections SET via_contact = ? WHERE via_contact = ? AND user_id = ?" (toContactId, fromContactId, userId)
DB.execute db "UPDATE group_members SET invited_by = ? WHERE invited_by = ? AND user_id = ?" (toContactId, fromContactId, userId)
DB.execute db "UPDATE messages SET contact_id = ? WHERE contact_id = ?" (toContactId, fromContactId)
DB.executeNamed
db
[sql|
UPDATE group_members
SET contact_id = :to_contact_id,
local_display_name = (SELECT local_display_name FROM contacts WHERE contact_id = :to_contact_id),
contact_profile_id = (SELECT contact_profile_id FROM contacts WHERE contact_id = :to_contact_id)
WHERE contact_id = :from_contact_id
AND user_id = :user_id
|]
[ ":to_contact_id" := toContactId,
":from_contact_id" := fromContactId,
":user_id" := userId
]
DB.execute db "DELETE FROM contacts WHERE contact_id = ? AND user_id = ?" (fromContactId, userId)
DB.execute db "DELETE FROM display_names WHERE local_display_name = ? AND user_id = ?" (localDisplayName, userId)
getConnectionChatDirection :: StoreMonad m => SQLiteStore -> User -> ConnId -> m (ChatDirection 'Agent) getConnectionChatDirection :: StoreMonad m => SQLiteStore -> User -> ConnId -> m (ChatDirection 'Agent)
getConnectionChatDirection st User {userId, userContactId} agentConnId = getConnectionChatDirection st User {userId, userContactId} agentConnId =
liftIOEither . withTransaction st $ \db -> runExceptT $ do liftIOEither . withTransaction st $ \db -> runExceptT $ do
@ -377,7 +506,7 @@ createNewGroup st gVar user groupProfile =
profileId <- insertedRowId db profileId <- insertedRowId db
DB.execute db "INSERT INTO groups (local_display_name, user_id, group_profile_id) VALUES (?, ?, ?)" (displayName, uId, profileId) DB.execute db "INSERT INTO groups (local_display_name, user_id, group_profile_id) VALUES (?, ?, ?)" (displayName, uId, profileId)
groupId <- insertedRowId db groupId <- insertedRowId db
memberId <- randomId gVar 12 memberId <- randomBytes gVar 12
membership <- createContactMember_ db user groupId user (memberId, GROwner) GCUserMember GSMemCreator IBUser membership <- createContactMember_ db user groupId user (memberId, GROwner) GCUserMember GSMemCreator IBUser
pure $ Right Group {groupId, localDisplayName = displayName, groupProfile, members = [], membership} pure $ Right Group {groupId, localDisplayName = displayName, groupProfile, members = [], membership}
@ -865,20 +994,23 @@ withLocalDisplayName db userId displayName action = getLdnSuffix >>= (`tryCreate
(ldn, displayName, ldnSuffix, userId) (ldn, displayName, ldnSuffix, userId)
createWithRandomId :: forall a. TVar ChaChaDRG -> (ByteString -> IO a) -> IO (Either StoreError a) createWithRandomId :: forall a. TVar ChaChaDRG -> (ByteString -> IO a) -> IO (Either StoreError a)
createWithRandomId gVar create = tryCreate 3 createWithRandomId = createWithRandomBytes 12
createWithRandomBytes :: forall a. Int -> TVar ChaChaDRG -> (ByteString -> IO a) -> IO (Either StoreError a)
createWithRandomBytes size gVar create = tryCreate 3
where where
tryCreate :: Int -> IO (Either StoreError a) tryCreate :: Int -> IO (Either StoreError a)
tryCreate 0 = pure $ Left SEUniqueID tryCreate 0 = pure $ Left SEUniqueID
tryCreate n = do tryCreate n = do
id' <- randomId gVar 12 id' <- randomBytes gVar size
E.try (create id') >>= \case E.try (create id') >>= \case
Right x -> pure $ Right x Right x -> pure $ Right x
Left e Left e
| DB.sqlError e == DB.ErrorConstraint -> tryCreate (n - 1) | DB.sqlError e == DB.ErrorConstraint -> tryCreate (n - 1)
| otherwise -> pure . Left . SEInternal $ bshow e | otherwise -> pure . Left . SEInternal $ bshow e
randomId :: TVar ChaChaDRG -> Int -> IO ByteString randomBytes :: TVar ChaChaDRG -> Int -> IO ByteString
randomId gVar n = B64.encode <$> (atomically . stateTVar gVar $ randomBytesGenerate n) randomBytes gVar n = B64.encode <$> (atomically . stateTVar gVar $ randomBytesGenerate n)
data StoreError data StoreError
= SEDuplicateName = SEDuplicateName

View File

@ -27,6 +27,8 @@ module Simplex.Chat.View
showUserJoinedGroup, showUserJoinedGroup,
showJoinedGroupMemberConnecting, showJoinedGroupMemberConnecting,
showConnectedToGroupMember, showConnectedToGroupMember,
showGroupMembers,
showContactsMerged,
safeDecodeUtf8, safeDecodeUtf8,
) )
where where
@ -118,6 +120,12 @@ showJoinedGroupMemberConnecting = printToView .:. joinedGroupMemberConnecting
showConnectedToGroupMember :: ChatReader m => GroupName -> GroupMember -> m () showConnectedToGroupMember :: ChatReader m => GroupName -> GroupMember -> m ()
showConnectedToGroupMember = printToView .: connectedToGroupMember showConnectedToGroupMember = printToView .: connectedToGroupMember
showGroupMembers :: ChatReader m => Group -> m ()
showGroupMembers = printToView . groupMembers
showContactsMerged :: ChatReader m => Contact -> Contact -> m ()
showContactsMerged = printToView .: contactsMerged
invitation :: SMPQueueInfo -> [StyledString] invitation :: SMPQueueInfo -> [StyledString]
invitation qInfo = invitation qInfo =
[ "pass this invitation to your contact (via another channel): ", [ "pass this invitation to your contact (via another channel): ",
@ -181,6 +189,32 @@ connectedMember m = case memberCategory m of
GCPostMember -> "new member " <> ttyMember m -- without fullName as as it was shown in joinedGroupMemberConnecting GCPostMember -> "new member " <> ttyMember m -- without fullName as as it was shown in joinedGroupMemberConnecting
_ -> "member " <> ttyMember m -- these case is not used _ -> "member " <> ttyMember m -- these case is not used
groupMembers :: Group -> [StyledString]
groupMembers Group {membership, members} = map groupMember . filter (not . removedOrLeft) $ membership : members
where
removedOrLeft m = let s = memberStatus m in s == GSMemRemoved || s == GSMemLeft
groupMember m = ttyFullMember m <> ": " <> role m <> ", " <> category m <> status m
role = plain . serializeMemberRole . memberRole
category m = case memberCategory m of
GCUserMember -> "you, "
GCInviteeMember -> "invited, "
GCHostMember -> "host, "
_ -> ""
status m = case memberStatus m of
GSMemRemoved -> "removed"
GSMemLeft -> "left"
GSMemInvited -> "not yet joined"
GSMemConnected -> "connected"
GSMemComplete -> "connected"
GSMemCreator -> "created group"
_ -> ""
contactsMerged :: Contact -> Contact -> [StyledString]
contactsMerged _to@Contact {localDisplayName = c1} _from@Contact {localDisplayName = c2} =
[ "contact " <> ttyContact c2 <> " is merged into " <> ttyContact c1,
"use " <> ttyToContact c1 <> highlight' "<message>" <> " to send messages"
]
receivedMessage :: StyledString -> UTCTime -> Text -> MsgIntegrity -> IO [StyledString] receivedMessage :: StyledString -> UTCTime -> Text -> MsgIntegrity -> IO [StyledString]
receivedMessage from utcTime msg mOk = do receivedMessage from utcTime msg mOk = do
t <- formatUTCTime <$> getCurrentTimeZone <*> getZonedTime t <- formatUTCTime <$> getCurrentTimeZone <*> getZonedTime

View File

@ -155,10 +155,15 @@ testGroup2 =
[ bob <## "#club: dan joined the group", [ bob <## "#club: dan joined the group",
do do
dan <## "#club: you joined the group" dan <## "#club: you joined the group"
dan <### ["#club: member alice_1 (Alice) is connected", "#club: member cath (Catherine) is connected"], dan
<### [ "#club: member alice_1 (Alice) is connected",
"#club: member cath (Catherine) is connected",
"use @alice <message> to send messages"
],
do do
alice <## "#club: bob added dan_1 (Daniel) to the group (connecting...)" alice <## "#club: bob added dan_1 (Daniel) to the group (connecting...)"
alice <## "#club: new member dan_1 is connected", alice <## "#club: new member dan_1 is connected"
alice <## "use @dan <message> to send messages",
do do
cath <## "#club: bob added dan (Daniel) to the group (connecting...)" cath <## "#club: bob added dan (Daniel) to the group (connecting...)"
cath <## "#club: new member dan is connected" cath <## "#club: new member dan is connected"
@ -167,7 +172,7 @@ testGroup2 =
concurrentlyN_ concurrentlyN_
[ bob <# "#club alice> hello", [ bob <# "#club alice> hello",
cath <# "#club alice> hello", cath <# "#club alice> hello",
dan <# "#club alice_1> hello" dan <# "#club alice> hello"
] ]
bob #> "#club hi there" bob #> "#club hi there"
concurrentlyN_ concurrentlyN_
@ -183,7 +188,7 @@ testGroup2 =
] ]
dan #> "#club how is it going?" dan #> "#club how is it going?"
concurrentlyN_ concurrentlyN_
[ alice <# "#club dan_1> how is it going?", [ alice <# "#club dan> how is it going?",
bob <# "#club dan> how is it going?", bob <# "#club dan> how is it going?",
cath <# "#club dan> how is it going?" cath <# "#club dan> how is it going?"
] ]
@ -195,10 +200,10 @@ testGroup2 =
cath <# "dan> hey cath" cath <# "dan> hey cath"
cath #> "@dan hey dan" cath #> "@dan hey dan"
dan <# "cath> hey dan" dan <# "cath> hey dan"
dan #> "@alice_1 hi alice" dan #> "@alice hi alice"
alice <# "dan_1> hi alice" alice <# "dan> hi alice"
alice #> "@dan_1 hello dan" alice #> "@dan hello dan"
dan <# "alice_1> hello dan" dan <# "alice> hello dan"
connectUsers :: TestCC -> TestCC -> IO () connectUsers :: TestCC -> TestCC -> IO ()
connectUsers cc1 cc2 = do connectUsers cc1 cc2 = do