started chat groups protocol (#75)

* create group

* add user as member, store methods to get group and to create group member

* add group member and send member invitation

* fix ci: use simplexmq from github

* chat protocol: create SMP agent connection when inviting member

* update protocol, started group invitation receiving
This commit is contained in:
Evgeny Poberezkin
2021-07-12 19:00:03 +01:00
committed by GitHub
parent 24c62584fc
commit e99c4bda1e
12 changed files with 575 additions and 191 deletions
+58 -38
View File
@@ -15,7 +15,8 @@ import Control.Logger.Simple
import Control.Monad.Except
import Control.Monad.IO.Unlift
import Control.Monad.Reader
import Data.Attoparsec.ByteString.Char8 (Parser, (<?>))
import Crypto.Random (drgNew)
import Data.Attoparsec.ByteString.Char8 (Parser)
import qualified Data.Attoparsec.ByteString.Char8 as A
import Data.Bifunctor (first)
import Data.ByteString.Char8 (ByteString)
@@ -42,12 +43,11 @@ import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..))
import Simplex.Messaging.Agent.Protocol
import Simplex.Messaging.Client (smpDefaultConfig)
import Simplex.Messaging.Parsers (parseAll)
import Simplex.Messaging.Util (bshow, raceAny_)
import Simplex.Messaging.Util (raceAny_)
import System.Exit (exitFailure)
import System.IO (hFlush, stdout)
import Text.Read (readMaybe)
import UnliftIO.Async (race_)
import qualified UnliftIO.Exception as E
import UnliftIO.STM
data ChatCommand
@@ -57,7 +57,7 @@ data ChatCommand
| Connect SMPQueueInfo
| DeleteContact ContactRef
| SendMessage ContactRef ByteString
| NewGroup GroupRef
| NewGroup GroupProfile
| AddMember GroupRef ContactRef GroupMemberRole
| RemoveMember GroupRef ContactRef
| MemberRole GroupRef ContactRef GroupMemberRole
@@ -97,9 +97,10 @@ newChatController ChatOpts {dbFile, smpServers} t sendNotification = do
currentUser <- getCreateActiveUser chatStore
chatTerminal <- newChatTerminal t
smpAgent <- getSMPAgentClient cfg {dbFile = dbFile <> ".agent.db", smpServers}
idsDrg <- newTVarIO =<< drgNew
inputQ <- newTBQueueIO $ tbqSize cfg
notifyQ <- newTBQueueIO $ tbqSize cfg
pure ChatController {currentUser, smpAgent, chatTerminal, chatStore, inputQ, notifyQ, sendNotification}
pure ChatController {currentUser, smpAgent, chatTerminal, chatStore, idsDrg, inputQ, notifyQ, sendNotification}
runSimplexChat :: ChatController -> IO ()
runSimplexChat = runReaderT (race_ runTerminalInput runChatController)
@@ -129,7 +130,7 @@ inputSubscriber = do
void . runExceptT $ processChatCommand user cmd `catchError` showChatError
processChatCommand :: ChatMonad m => User -> ChatCommand -> m ()
processChatCommand User {userId, profile} = \case
processChatCommand user@User {userId, profile} = \case
ChatHelp -> printToView chatHelpInfo
MarkdownHelp -> printToView markdownInfo
AddContact -> do
@@ -137,30 +138,49 @@ processChatCommand User {userId, profile} = \case
withStore $ \st -> createDirectConnection st userId connId
showInvitation qInfo
Connect qInfo -> do
connId <- withAgent $ \agent -> joinConnection agent qInfo $ encodeProfile profile
connId <- withAgent $ \a -> joinConnection a qInfo $ encodeProfile profile
withStore $ \st -> createDirectConnection st userId connId
DeleteContact cRef -> do
conns <- withStore $ \st -> getContactConnections st userId cRef
withAgent $ \smp -> forM_ conns $ \Connection {agentConnId} ->
deleteConnection smp agentConnId `catchError` \(_ :: AgentErrorType) -> pure ()
withAgent $ \a -> forM_ conns $ \Connection {agentConnId} ->
deleteConnection a agentConnId `catchError` \(_ :: AgentErrorType) -> pure ()
withStore $ \st -> deleteContact st userId cRef
unsetActive $ ActiveC cRef
when (null conns) . throwError . ChatErrorContact $ CENotFound cRef
showContactDeleted cRef
SendMessage cRef msg -> do
Connection {agentConnId} <- withStore $ \st -> getContactConnection st userId cRef
contact <- withStore $ \st -> getContact st userId cRef
let body = MsgBodyContent {contentType = SimplexContentType XCText, contentData = msg}
rawMsg = rawChatMessage ChatMessage {chatMsgId = Nothing, chatMsgEvent = XMsgNew MTText [] [body], chatDAG = Nothing}
void . withAgent $ \smp -> sendMessage smp agentConnId $ serializeRawChatMessage rawMsg
connId = contactConnId contact
void . withAgent $ \a -> sendMessage a connId $ serializeRawChatMessage rawMsg
setActive $ ActiveC cRef
NewGroup _gRef -> pure ()
AddMember _gRef _cRef _mRole -> pure ()
NewGroup gProfile -> do
gVar <- asks idsDrg
void $ withStore $ \st -> createNewGroup st gVar user gProfile
showGroupCreated gProfile
AddMember gRef cRef memRole -> do
(group, contact) <- withStore $ \st -> (,) <$> getGroup st user gRef <*> getContact st userId cRef
let Group {groupId, groupProfile, membership, members} = group
userRole = memberRole membership
userMemberId = memberId membership
when (userRole < GRAdmin || userRole < memRole) $ throwError $ ChatError CEGroupRole
when (isMember contact members) $ throwError $ ChatError CEGroupDuplicateMember
gVar <- asks idsDrg
(agentConnId, qInfo) <- withAgent createConnection
memberId <- withStore $ \st -> createGroupMember st gVar user groupId (contactId contact) memRole IBUser agentConnId
let chatMsgEvent = XGrpInv (userMemberId, userRole) (memberId, memRole) qInfo groupProfile
rawMsg = rawChatMessage ChatMessage {chatMsgId = Nothing, chatMsgEvent, chatDAG = Nothing}
connId = contactConnId contact
void . withAgent $ \a -> sendMessage a connId $ serializeRawChatMessage rawMsg
MemberRole _gRef _cRef _mRole -> pure ()
RemoveMember _gRef _cRef -> pure ()
LeaveGroup _gRef -> pure ()
DeleteGroup _gRef -> pure ()
ListMembers _gRef -> pure ()
SendGroupMessage _gRef _msg -> pure ()
where
isMember :: Contact -> [(GroupMember, Connection)] -> Bool
isMember Contact {contactId} members = isJust $ find ((== Just contactId) . memberContactId . fst) members
agentSubscriber :: (MonadUnliftIO m, MonadReader ChatController m) => m ()
agentSubscriber = do
@@ -175,13 +195,14 @@ processAgentMessage :: forall m. ChatMonad m => User -> ConnId -> ACommand 'Agen
processAgentMessage User {userId, profile} agentConnId agentMessage = do
chatDirection <- withStore $ \st -> getConnectionChatDirection st userId agentConnId
case chatDirection of
ReceivedDirectMessage Contact {localContactRef = c} ->
ReceivedDirectMessage (CContact ct@Contact {localContactRef = c}) ->
case agentMessage of
MSG meta msgBody -> do
ChatMessage {chatMsgEvent} <- liftEither $ parseChatMessage msgBody
case chatMsgEvent of
XMsgNew MTText [] body -> newTextMessage c meta $ find (isSimplexContentType XCText) body
XInfo _ -> pure () -- TODO profile update
XGrpInv fromMem invMem qInfo groupProfile -> groupInvitation ct fromMem invMem qInfo groupProfile
_ -> pure ()
CON -> do
-- TODO update connection status
@@ -193,14 +214,14 @@ processAgentMessage User {userId, profile} agentConnId agentMessage = do
showToast ("@" <> c) "disconnected"
unsetActive $ ActiveC c
_ -> pure ()
ReceivedDirectMessage NewContact {activeConn} ->
ReceivedDirectMessage (CConnection conn) ->
case agentMessage of
CONF confId connInfo -> do
-- TODO update connection status
saveConnInfo activeConn connInfo
saveConnInfo conn connInfo
withAgent $ \a -> allowConnection a agentConnId confId $ encodeProfile profile
INFO connInfo ->
saveConnInfo activeConn connInfo
saveConnInfo conn connInfo
_ -> pure ()
_ -> pure ()
where
@@ -213,6 +234,11 @@ processAgentMessage User {userId, profile} agentConnId agentMessage = do
setActive $ ActiveC c
_ -> pure ()
groupInvitation :: Contact -> (MemberId, GroupMemberRole) -> (MemberId, GroupMemberRole) -> SMPQueueInfo -> GroupProfile -> m ()
groupInvitation _ct (fromMemId, fromRole) (memId, memRole) _qInfo _groupProfile = do
when (fromRole < GRAdmin || fromRole < memRole) $ throwError $ ChatError CEGroupRole
when (fromMemId == memId) $ throwError $ ChatError CEGroupDuplicateMember
parseChatMessage :: ByteString -> Either ChatError ChatMessage
parseChatMessage msgBody = first ChatErrorMessage (parseAll rawChatMessageP msgBody >>= toChatMessage)
@@ -305,26 +331,16 @@ withStore ::
ChatMonad m =>
(forall m'. (MonadUnliftIO m', MonadError StoreError m') => SQLiteStore -> m' a) ->
m a
withStore action = do
st <- asks chatStore
runExceptT (action st `E.catch` handleInternal) >>= \case
Right c -> pure c
Left e -> throwError $ storeError e
where
-- TODO when parsing exception happens in store, the agent hangs;
-- changing SQLError to SomeException does not help
handleInternal :: (MonadError StoreError m') => E.SomeException -> m' a
handleInternal e = throwError . SEInternal $ bshow e
storeError :: StoreError -> ChatError
storeError = \case
SEContactNotFound c -> ChatErrorContact $ CENotFound c
e -> ChatErrorStore e
withStore action =
asks chatStore
>>= runExceptT . action
>>= liftEither . first ChatErrorStore
chatCommandP :: Parser ChatCommand
chatCommandP =
("/help" <|> "/h") $> ChatHelp
<|> ("/group #" <|> "/g #") *> (NewGroup <$> groupRef)
<|> ("/add #" <|> "/a #") *> (AddMember <$> groupRef <* A.space <*> contactRef <* A.space <*> memberRole)
<|> ("/group #" <|> "/g #") *> (NewGroup <$> groupProfile)
<|> ("/add #" <|> "/a #") *> (AddMember <$> groupRef <* A.space <*> contactRef <*> memberRole)
<|> ("/remove #" <|> "/rm #") *> (RemoveMember <$> groupRef <* A.space <*> contactRef)
<|> ("/delete #" <|> "/d #") *> (DeleteGroup <$> groupRef)
<|> ("/members #" <|> "/ms #") *> (ListMembers <$> groupRef)
@@ -338,8 +354,12 @@ chatCommandP =
contactRef = safeDecodeUtf8 <$> (B.cons <$> A.satisfy refChar <*> A.takeTill (== ' '))
refChar c = c > ' ' && c /= '#' && c /= '@'
groupRef = contactRef
groupProfile = do
gRef <- groupRef
gName <- safeDecodeUtf8 <$> (A.space *> A.takeByteString) <|> pure ""
pure GroupProfile {groupRef = gRef, displayName = if T.null gName then gRef else gName}
memberRole =
("owner" $> GROwner)
<|> ("admin" $> GRAdmin)
<|> ("normal" $> GRMember)
<?> "memberRole"
(" owner" $> GROwner)
<|> (" admin" $> GRAdmin)
<|> (" normal" $> GRMember)
<|> pure GRMember
+4 -3
View File
@@ -10,6 +10,7 @@ import Control.Exception
import Control.Monad.Except
import Control.Monad.IO.Unlift
import Control.Monad.Reader
import Crypto.Random (ChaChaDRG)
import Simplex.Chat.Notification
import Simplex.Chat.Store (StoreError)
import Simplex.Chat.Terminal
@@ -24,6 +25,7 @@ data ChatController = ChatController
smpAgent :: AgentClient,
chatTerminal :: ChatTerminal,
chatStore :: SQLiteStore,
idsDrg :: TVar ChaChaDRG,
inputQ :: TBQueue InputEvent,
notifyQ :: TBQueue Notification,
sendNotification :: Notification -> IO ()
@@ -32,14 +34,13 @@ data ChatController = ChatController
data InputEvent = InputCommand String | InputControl Char
data ChatError
= ChatErrorContact ContactError
= ChatError ChatErrorType
| ChatErrorMessage String
| ChatErrorAgent AgentErrorType
| ChatErrorStore StoreError
deriving (Show, Exception)
data ContactError = CENotFound ContactRef | CEProfile String
deriving (Show, Exception)
data ChatErrorType = CEGroupRole | CEGroupDuplicateMember deriving (Show, Exception)
type ChatMonad m = (MonadUnliftIO m, MonadReader ChatController m, MonadError ChatError m)
+48 -52
View File
@@ -12,7 +12,6 @@ module Simplex.Chat.Protocol where
import Control.Applicative (optional)
import Control.Monad ((<=<))
import Control.Monad.Except (throwError)
import Data.Aeson (FromJSON, ToJSON)
import qualified Data.Aeson as J
import Data.Attoparsec.ByteString.Char8 (Parser)
@@ -30,44 +29,38 @@ import Simplex.Messaging.Parsers (parseAll)
import Simplex.Messaging.Util (bshow)
data ChatDirection (p :: AParty) where
ReceivedDirectMessage :: Contact -> ChatDirection 'Agent
SentDirectMessage :: Contact -> ChatDirection 'Client
ReceivedGroupMessage :: Group -> Contact -> ChatDirection 'Agent
ReceivedDirectMessage :: ConnContact -> ChatDirection 'Agent
SentDirectMessage :: ConnContact -> ChatDirection 'Client
ReceivedGroupMessage :: Group -> ConnContact -> ChatDirection 'Agent
SentGroupMessage :: Group -> ChatDirection 'Client
deriving instance Eq (ChatDirection p)
deriving instance Show (ChatDirection p)
data ConnContact = CContact Contact | CConnection Connection
deriving (Eq, Show)
data ChatMsgEvent
= XMsgNew {messageType :: MessageType, files :: [(ContentType, Int)], content :: [MsgBodyContent]}
= XMsgNew
{ messageType :: MessageType,
files :: [(ContentType, Int)],
content :: [MsgBodyContent]
}
| XInfo Profile
| XGrpInv InvitationId MemberId GroupMemberRole GroupProfile
| XGrpAcpt InvitationId SMPQueueInfo
| XGrpInv
{ fromMember :: (MemberId, GroupMemberRole),
invitedMember :: (MemberId, GroupMemberRole),
queueInfo :: SMPQueueInfo,
groupProfile :: GroupProfile
}
| XGrpAcpt MemberId
| XGrpMemNew MemberId GroupMemberRole Profile
| XGrpMemIntro MemberId GroupMemberRole Profile
deriving (Eq, Show)
type MemberId = ByteString
data MessageType = MTText | MTImage deriving (Eq, Show)
data GroupMemberRole = GROwner | GRAdmin | GRMember
deriving (Eq, Show)
toMemberRole :: ByteString -> Either String GroupMemberRole
toMemberRole = \case
"owner" -> Right GROwner
"admin" -> Right GRAdmin
"member" -> Right GRMember
r -> Left $ "invalid group member role " <> B.unpack r
serializeMemberRole :: GroupMemberRole -> ByteString
serializeMemberRole = \case
GROwner -> "owner"
GRAdmin -> "admin"
GRMember -> "member"
toMsgType :: ByteString -> Either String MessageType
toMsgType = \case
"c.text" -> Right MTText
@@ -96,41 +89,38 @@ toChatMessage RawChatMessage {chatMsgId, chatMsgEvent, chatMsgParams, chatMsgBod
files <- mapM (toContentInfo <=< parseAll contentInfoP) rawFiles
let msg = XMsgNew {messageType = t, files, content = body}
pure ChatMessage {chatMsgId, chatMsgEvent = msg, chatDAG}
[] -> throwError "x.msg.new expects at least one parameter"
[] -> Left "x.msg.new expects at least one parameter"
"x.info" -> case chatMsgParams of
[] -> do
profile <- getJSON body
pure ChatMessage {chatMsgId, chatMsgEvent = XInfo profile, chatDAG}
_ -> throwError "x.info expects no parameters"
_ -> Left "x.info expects no parameters"
"x.grp.inv" -> case chatMsgParams of
[invId', memId', role'] -> do
invId <- B64.decode invId'
memId <- B64.decode memId'
role <- toMemberRole role'
groupProfile <- getJSON body
pure ChatMessage {chatMsgId, chatMsgEvent = XGrpInv invId memId role groupProfile, chatDAG}
_ -> throwError "x.grp.inv expects 3 parameters"
[fromMemId, fromRole, memId, role, qInfo] -> do
fromMember <- (,) <$> B64.decode fromMemId <*> toMemberRole fromRole
invitedMember <- (,) <$> B64.decode memId <*> toMemberRole role
msg <- XGrpInv fromMember invitedMember <$> parseAll smpQueueInfoP qInfo <*> getJSON body
pure ChatMessage {chatMsgId, chatMsgEvent = msg, chatDAG}
_ -> Left "x.grp.inv expects 5 parameters"
"x.grp.acpt" -> case chatMsgParams of
[invId, qInfo] -> do
msg <- XGrpAcpt <$> B64.decode invId <*> parseAll smpQueueInfoP qInfo
[memId] -> do
msg <- XGrpAcpt <$> B64.decode memId
pure ChatMessage {chatMsgId, chatMsgEvent = msg, chatDAG}
_ -> throwError "x.grp.acpt expects 2 parameters"
"x.grp.mem.new" -> case chatMsgParams of
[memId, role] -> do
msg <- XGrpMemNew <$> B64.decode memId <*> toMemberRole role <*> getJSON body
pure ChatMessage {chatMsgId, chatMsgEvent = msg, chatDAG}
_ -> throwError "x.grp.acpt expects 2 parameters"
"x.grp.mem.intro" -> case chatMsgParams of
[memId, role] -> do
msg <- XGrpMemIntro <$> B64.decode memId <*> toMemberRole role <*> getJSON body
pure ChatMessage {chatMsgId, chatMsgEvent = msg, chatDAG}
_ -> throwError "x.grp.acpt expects 2 parameters"
_ -> throwError $ "unsupported event " <> B.unpack chatMsgEvent
_ -> Left "x.grp.acpt expects one parameter"
"x.grp.mem.new" -> memberMessage chatMsgParams XGrpMemNew body chatDAG
"x.grp.mem.intro" -> memberMessage chatMsgParams XGrpMemIntro body chatDAG
_ -> Left $ "unsupported event " <> B.unpack chatMsgEvent
where
getDAG :: [MsgBodyContent] -> (Maybe ByteString, [MsgBodyContent])
getDAG body = case break (isContentType SimplexDAG) body of
(b, MsgBodyContent SimplexDAG dag : a) -> (Just dag, b <> a)
_ -> (Nothing, body)
memberMessage ::
FromJSON a => [ByteString] -> (MemberId -> GroupMemberRole -> a -> ChatMsgEvent) -> [MsgBodyContent] -> Maybe ByteString -> Either String ChatMessage
memberMessage [memId, role] mkMsg body chatDAG = do
msg <- mkMsg <$> B64.decode memId <*> toMemberRole role <*> getJSON body
pure ChatMessage {chatMsgId, chatMsgEvent = msg, chatDAG}
memberMessage _ _ _ _ = Left "message expects 2 parameters"
toContentInfo :: (RawContentType, Int) -> Either String (ContentType, Int)
toContentInfo (rawType, size) = (,size) <$> toContentType rawType
getJSON :: FromJSON a => [MsgBodyContent] -> Either String a
@@ -161,12 +151,18 @@ rawChatMessage ChatMessage {chatMsgId, chatMsgEvent, chatDAG} =
XInfo profile ->
let chatMsgBody = rawWithDAG [jsonBody profile]
in RawChatMessage {chatMsgId, chatMsgEvent = "x.info", chatMsgParams = [], chatMsgBody}
XGrpInv invId memId role groupProfile ->
let chatMsgParams = [B64.encode invId, B64.encode memId, serializeMemberRole role]
XGrpInv (fromMemId, fromRole) (memId, role) qInfo groupProfile ->
let chatMsgParams =
[ B64.encode fromMemId,
serializeMemberRole fromRole,
B64.encode memId,
serializeMemberRole role,
serializeSmpQueueInfo qInfo
]
chatMsgBody = rawWithDAG [jsonBody groupProfile]
in RawChatMessage {chatMsgId, chatMsgEvent = "x.grp.inv", chatMsgParams, chatMsgBody}
XGrpAcpt invId qInfo ->
let chatMsgParams = [B64.encode invId, serializeSmpQueueInfo qInfo]
XGrpAcpt memId ->
let chatMsgParams = [B64.encode memId]
in RawChatMessage {chatMsgId, chatMsgEvent = "x.grp.acpt", chatMsgParams, chatMsgBody = []}
XGrpMemNew memId role profile ->
let chatMsgParams = [B64.encode memId, serializeMemberRole role]
+302 -52
View File
@@ -8,6 +8,7 @@
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeOperators #-}
module Simplex.Chat.Store
( SQLiteStore,
@@ -19,16 +20,23 @@ module Simplex.Chat.Store
createDirectConnection,
createDirectContact,
deleteContact,
getContactConnection,
getContact,
getContactConnections,
getConnectionChatDirection,
createNewGroup,
createGroup,
getGroup,
createGroupMember,
)
where
import Control.Concurrent.STM (stateTVar)
import Control.Exception (Exception)
import qualified Control.Exception as E
import Control.Monad.Except
import Control.Monad.IO.Unlift
import Crypto.Random (ChaChaDRG, randomBytesGenerate)
import qualified Data.ByteString.Base64 as B64
import Data.ByteString.Char8 (ByteString)
import Data.FileEmbed (embedDir, makeRelativeToProject)
import Data.Function (on)
@@ -39,7 +47,7 @@ import Data.Text (Text)
import qualified Data.Text as T
import Data.Text.Encoding (decodeUtf8)
import Data.Time.Clock (UTCTime)
import Database.SQLite.Simple (NamedParam (..), Only (..), SQLError)
import Database.SQLite.Simple (NamedParam (..), Only (..), SQLError, (:.) (..))
import qualified Database.SQLite.Simple as DB
import Database.SQLite.Simple.QQ (sql)
import Simplex.Chat.Protocol
@@ -49,6 +57,7 @@ import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore (..), createSQLiteStore
import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..))
import Simplex.Messaging.Util (bshow, liftIOEither, (<$$>))
import System.FilePath (takeBaseName, takeExtension)
import UnliftIO.STM
-- | The list of migrations in ascending order by date
migrations :: [Migration]
@@ -71,22 +80,22 @@ handleSQLError err e
| otherwise = SEInternal $ bshow e
insertedRowId :: DB.Connection -> IO Int64
insertedRowId db = fromOnly . head <$> DB.query_ db "SELECT last_insert_rowid();"
insertedRowId db = fromOnly . head <$> DB.query_ db "SELECT last_insert_rowid()"
createUser :: (MonadUnliftIO m, MonadError StoreError m) => SQLiteStore -> Profile -> Bool -> m User
createUser st Profile {contactRef, displayName} activeUser =
liftIOEither . checkConstraint SEDuplicateContactRef . withTransaction st $ \db -> do
DB.execute db "INSERT INTO contact_profiles (contact_ref, display_name) VALUES (?, ?);" (contactRef, displayName)
DB.execute db "INSERT INTO contact_profiles (contact_ref, display_name) VALUES (?, ?)" (contactRef, displayName)
profileId <- insertedRowId db
DB.execute db "INSERT INTO users (contact_id, active_user) VALUES (0, ?);" (Only activeUser)
DB.execute db "INSERT INTO users (contact_id, active_user) VALUES (0, ?)" (Only activeUser)
userId <- insertedRowId db
DB.execute
db
"INSERT INTO contacts (contact_profile_id, local_contact_ref, lcr_base, user_id, user) VALUES (?, ?, ?, ?, 1);"
(profileId, contactRef, contactRef, userId)
"INSERT INTO contacts (contact_profile_id, local_contact_ref, lcr_base, user_id, is_user) VALUES (?, ?, ?, ?, ?)"
(profileId, contactRef, contactRef, userId, True)
contactId <- insertedRowId db
DB.execute db "UPDATE users SET contact_id = ? WHERE user_id = ?;" (contactId, userId)
pure . Right $ toUser (userId, activeUser, contactRef, displayName)
DB.execute db "UPDATE users SET contact_id = ? WHERE user_id = ?" (contactId, userId)
pure . Right $ toUser (userId, contactId, activeUser, contactRef, displayName)
getUsers :: SQLiteStore -> IO [User]
getUsers st =
@@ -95,22 +104,22 @@ getUsers st =
<$> DB.query_
db
[sql|
SELECT u.user_id, u.active_user, c.local_contact_ref, p.display_name
SELECT u.user_id, u.contact_id, u.active_user, c.local_contact_ref, p.display_name
FROM users u
JOIN contacts c ON u.contact_id = c.contact_id
JOIN contact_profiles p ON c.contact_profile_id = p.contact_profile_id
|]
toUser :: (UserId, Bool, ContactRef, Text) -> User
toUser (userId, activeUser, contactRef, displayName) =
toUser :: (UserId, Int64, Bool, ContactRef, Text) -> User
toUser (userId, userContactId, activeUser, contactRef, displayName) =
let profile = Profile {contactRef, displayName}
in User {userId, localContactRef = contactRef, profile, activeUser}
in User {userId, userContactId, localContactRef = contactRef, profile, activeUser}
setActiveUser :: MonadUnliftIO m => SQLiteStore -> UserId -> m ()
setActiveUser st userId = do
liftIO . withTransaction st $ \db -> do
DB.execute_ db "UPDATE users SET active_user = 0;"
DB.execute db "UPDATE users SET active_user = 1 WHERE user_id = ?;" (Only userId)
DB.execute_ db "UPDATE users SET active_user = 0"
DB.execute db "UPDATE users SET active_user = 1 WHERE user_id = ?" (Only userId)
createDirectConnection :: MonadUnliftIO m => SQLiteStore -> UserId -> ConnId -> m ()
createDirectConnection st userId agentConnId =
@@ -127,7 +136,7 @@ createDirectContact ::
(MonadUnliftIO m, MonadError StoreError m) => SQLiteStore -> UserId -> Connection -> Profile -> m ()
createDirectContact st userId Connection {connId} Profile {contactRef, displayName} =
liftIOEither . withTransaction st $ \db -> do
DB.execute db "INSERT INTO contact_profiles (contact_ref, display_name) VALUES (?, ?);" (contactRef, displayName)
DB.execute db "INSERT INTO contact_profiles (contact_ref, display_name) VALUES (?, ?)" (contactRef, displayName)
profileId <- insertedRowId db
lcrSuffix <- getLcrSuffix db
create db profileId lcrSuffix 20
@@ -141,7 +150,7 @@ createDirectContact st userId Connection {connId} Profile {contactRef, displayNa
SELECT lcr_suffix FROM contacts
WHERE user_id = :user_id AND lcr_base = :contact_ref
ORDER BY lcr_suffix DESC
LIMIT 1;
LIMIT 1
|]
[":user_id" := userId, ":contact_ref" := contactRef]
create :: DB.Connection -> Int64 -> Int -> Int -> IO (Either StoreError ())
@@ -179,24 +188,62 @@ deleteContact st userId contactRef =
FROM connections c
JOIN contacts cs ON c.contact_id = cs.contact_id
WHERE cs.user_id = :user_id AND cs.local_contact_ref = :contact_ref
);
)
|]
[":user_id" := userId, ":contact_ref" := contactRef]
DB.executeNamed
db
[sql|
DELETE FROM contacts
WHERE user_id = :user_id AND local_contact_ref = :contact_ref;
WHERE user_id = :user_id AND local_contact_ref = :contact_ref
|]
[":user_id" := userId, ":contact_ref" := contactRef]
-- TODO return the last connection that is ready, not any last connection
-- requires updating connection status
getContactConnection ::
(MonadUnliftIO m, MonadError StoreError m) => SQLiteStore -> UserId -> ContactRef -> m Connection
getContactConnection st userId contactRef =
getContact ::
(MonadUnliftIO m, MonadError StoreError m) => SQLiteStore -> UserId -> ContactRef -> m Contact
getContact st userId localContactRef =
liftIOEither . withTransaction st $ \db -> runExceptT $ do
c@Contact {contactId} <- getContact_ db
activeConn <- getConnection_ db contactId
pure $ (c :: Contact) {activeConn}
where
getContact_ db = ExceptT $ do
toContact
<$> DB.queryNamed
db
[sql|
SELECT c.contact_id, p.contact_ref, p.display_name
FROM contacts c
JOIN contact_profiles p ON c.contact_profile_id = p.contact_profile_id
WHERE c.user_id = :user_id AND c.local_contact_ref = :local_contact_ref AND c.is_user = :is_user
|]
[":user_id" := userId, ":local_contact_ref" := localContactRef, ":is_user" := False]
getConnection_ db contactId = ExceptT $ do
connection
<$> DB.queryNamed
db
[sql|
SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact,
c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.created_at
FROM connections c
WHERE c.user_id = :user_id AND c.contact_id == :contact_id
ORDER BY c.connection_id DESC
LIMIT 1
|]
[":user_id" := userId, ":contact_id" := contactId]
toContact [(contactId, contactRef, displayName)] =
let profile = Profile {contactRef, displayName}
in Right Contact {contactId, localContactRef, profile, activeConn = undefined}
toContact _ = Left $ SEContactNotFound localContactRef
connection (connRow : _) = Right $ toConnection connRow
connection _ = Left $ SEContactNotReady localContactRef
getContactConnections :: (MonadUnliftIO m, MonadError StoreError m) => SQLiteStore -> UserId -> ContactRef -> m [Connection]
getContactConnections st userId contactRef =
liftIOEither . withTransaction st $ \db ->
connection
connections
<$> DB.queryNamed
db
[sql|
@@ -207,33 +254,15 @@ getContactConnection st userId contactRef =
WHERE c.user_id = :user_id
AND cs.user_id = :user_id
AND cs.local_contact_ref == :contact_ref
ORDER BY c.connection_id DESC
LIMIT 1;
|]
[":user_id" := userId, ":contact_ref" := contactRef]
where
connection (connRow : _) = Right $ toConnection connRow
connection _ = Left $ SEContactNotFound contactRef
connections [] = Left $ SEContactNotFound contactRef
connections rows = Right $ map toConnection rows
getContactConnections :: MonadUnliftIO m => SQLiteStore -> UserId -> ContactRef -> m [Connection]
getContactConnections st userId contactRef =
liftIO . withTransaction st $ \db ->
map toConnection
<$> DB.queryNamed
db
[sql|
SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact,
c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.created_at
FROM connections c
JOIN contacts cs ON c.contact_id == cs.contact_id
WHERE c.user_id = :user_id
AND cs.user_id = :user_id
AND cs.local_contact_ref == :contact_ref;
|]
[":user_id" := userId, ":contact_ref" := contactRef]
type ConnectionRow = (Int64, ConnId, Int, Maybe Int64, ConnStatus, ConnType, Maybe Int64, Maybe Int64, UTCTime)
toConnection ::
(Int64, ConnId, Int, Maybe Int64, ConnStatus, ConnType, Maybe Int64, Maybe Int64, UTCTime) -> Connection
toConnection :: ConnectionRow -> Connection
toConnection (connId, agentConnId, connLevel, viaContact, connStatus, connType, contactId, groupMemberId, createdAt) =
let entityId = entityId_ connType
in Connection {connId, agentConnId, connLevel, viaContact, connStatus, connType, entityId, createdAt}
@@ -246,16 +275,16 @@ getConnectionChatDirection ::
(MonadUnliftIO m, MonadError StoreError m) => SQLiteStore -> UserId -> ConnId -> m (ChatDirection 'Agent)
getConnectionChatDirection st userId agentConnId =
liftIOEither . withTransaction st $ \db -> do
getConnection db >>= \case
getConnection_ db >>= \case
Left e -> pure $ Left e
Right c@Connection {connType, entityId} -> case connType of
ConnMember -> pure . Left $ SEInternal "group members not supported yet"
ConnContact ->
ReceivedDirectMessage <$$> case entityId of
Nothing -> pure $ Right NewContact {activeConn = c}
Just cId -> getContact db cId c
Nothing -> pure . Right $ CConnection c
Just cId -> getContact_ db cId c
where
getConnection db =
getConnection_ db =
connection
<$> DB.query
db
@@ -263,12 +292,12 @@ getConnectionChatDirection st userId agentConnId =
SELECT connection_id, agent_conn_id, conn_level, via_contact,
conn_status, conn_type, contact_id, group_member_id, created_at
FROM connections
WHERE user_id = ? AND agent_conn_id = ?;
WHERE user_id = ? AND agent_conn_id = ?
|]
(userId, agentConnId)
connection (connRow : _) = Right $ toConnection connRow
connection _ = Left $ SEConnectionNotFound agentConnId
getContact db contactId c =
getContact_ db contactId c =
toContact contactId c
<$> DB.query
db
@@ -281,12 +310,233 @@ getConnectionChatDirection st userId agentConnId =
(userId, contactId)
toContact contactId c [(localContactRef, contactRef, displayName)] =
let profile = Profile {contactRef, displayName}
in Right Contact {contactId, localContactRef, profile, activeConn = c}
in Right $ CContact Contact {contactId, localContactRef, profile, activeConn = c}
toContact _ _ _ = Left $ SEInternal "referenced contact not found"
-- | creates completely new group with a single member - the current user
createNewGroup :: (MonadUnliftIO m, MonadError StoreError m) => SQLiteStore -> TVar ChaChaDRG -> User -> GroupProfile -> m Group
createNewGroup st gVar User {userId, userContactId, profile} p@GroupProfile {groupRef, displayName} =
liftIOEither . checkConstraint SEDuplicateGroupRef . withTransaction st $ \db -> do
-- group inserted before profile to ensure its local_group_ref is unique
DB.execute db "INSERT INTO groups (local_group_ref, lgr_base, user_id) VALUES (?, ?, ?)" (groupRef, groupRef, userId)
groupId <- insertedRowId db
DB.execute db "INSERT INTO group_profiles (group_ref, display_name) VALUES (?, ?)" (groupRef, displayName)
profileId <- insertedRowId db
DB.execute db "UPDATE groups SET group_profile_id = ? WHERE group_id = ?" (profileId, groupId)
memberId <- randomId gVar 12
createMember_ db groupId userContactId GROwner GSMemReady (Just userContactId) memberId
groupMemberId <- insertedRowId db
let membership =
GroupMember
{ groupMemberId,
memberId,
memberRole = GROwner,
memberStatus = GSMemReady,
invitedBy = IBUser,
memberProfile = profile,
memberContactId = Just userContactId
}
pure $ Right Group {groupId, localGroupRef = groupRef, groupProfile = p, members = [], membership}
-- | creates a new group record for the group the current user was invited to
createGroup :: (MonadUnliftIO m, MonadError StoreError m) => SQLiteStore -> TVar ChaChaDRG -> User -> Contact -> GroupProfile -> m Group
createGroup st gVar User {userId, userContactId, profile} contact p@GroupProfile {groupRef, displayName} =
liftIOEither . withTransaction st $ \db -> do
DB.execute db "INSERT INTO group_profiles (group_ref, display_name) VALUES (?, ?)" (groupRef, displayName)
profileId <- insertedRowId db
lgrSuffix <- getLgrSuffix db
group <- create db profileId lgrSuffix 20
pure group
where
-- createMember_ db groupId userContactId GROwner GSMemReady (Just userContactId) memberId
-- groupMemberId <- insertedRowId db
-- let membership =
-- GroupMember
-- { groupMemberId,
-- memberId,
-- memberRole = GROwner,
-- memberStatus = GSMemReady,
-- invitedBy = IBUser,
-- memberProfile = profile,
-- memberContactId = Just userContactId
-- }
-- pure $ Right Group {groupId, localGroupRef = groupRef, groupProfile = p, members = [], membership}
getLgrSuffix :: DB.Connection -> IO Int
getLgrSuffix db =
maybe 0 ((+ 1) . fromOnly) . listToMaybe
<$> DB.queryNamed
db
[sql|
SELECT lgr_suffix FROM groups
WHERE user_id = :user_id AND lgr_base = :group_ref
ORDER BY lgr_suffix DESC
LIMIT 1
|]
[":user_id" := userId, ":group_ref" := groupRef]
create :: DB.Connection -> Int64 -> Int -> Int -> IO (Either StoreError Group)
create _ _ _ 0 = pure $ Left SEDuplicateGroupRef
create db profileId lgrSuffix attempts = do
let lgr = localGroupRef' lgrSuffix
E.try (insertGroup lgr) >>= \case
Right () -> do
groupId <- insertedRowId db
pure $ Right Group {groupId, localGroupRef = lgr, groupProfile = p, members = undefined, membership = undefined}
Left e
| DB.sqlError e == DB.ErrorConstraint -> create db profileId (lgrSuffix + 1) (attempts - 1)
| otherwise -> E.throwIO e
where
localGroupRef' 0 = groupRef
localGroupRef' n = groupRef <> T.pack ('_' : show n)
insertGroup lgr =
DB.execute
db
[sql|
INSERT INTO groups
(group_profile_id, local_group_ref, lgr_base, lgr_suffix, user_id) VALUES (?, ?, ?, ?, ?)
|]
(profileId, lgr, groupRef, lgrSuffix, userId)
-- TODO return the last connection that is ready, not any last connection
-- requires updating connection status
getGroup :: (MonadUnliftIO m, MonadError StoreError m) => SQLiteStore -> User -> GroupRef -> m Group
getGroup st User {userId, userContactId} localGroupRef =
liftIOEither . withTransaction st $ \db -> runExceptT $ do
g@Group {groupId} <- getGroup_ db
members <- getMembers_ db groupId
membership <- getUserMember_ db groupId
pure g {members, membership}
where
getGroup_ :: DB.Connection -> ExceptT StoreError IO Group
getGroup_ db = ExceptT $ do
toGroup
<$> DB.query
db
[sql|
SELECT g.group_id, p.group_ref, p.display_name
FROM groups g
JOIN group_profiles p ON p.group_profile_id = g.group_profile_id
WHERE g.local_group_ref = ? AND g.user_id = ?
|]
(localGroupRef, userId)
toGroup :: [(Int64, GroupRef, Text)] -> Either StoreError Group
toGroup [(groupId, groupRef, displayName)] =
let groupProfile = GroupProfile {groupRef, displayName}
in Right Group {groupId, localGroupRef, groupProfile, members = undefined, membership = undefined}
toGroup _ = Left $ SEGroupNotFound localGroupRef
getMembers_ :: DB.Connection -> Int64 -> ExceptT StoreError IO [(GroupMember, Connection)]
getMembers_ db groupId = ExceptT $ do
Right . map toContactMember
<$> DB.query
db
[sql|
SELECT
m.group_member_id, m.member_id, m.member_role, m.member_status,
m.invited_by, m.contact_id, p.contact_ref, p.display_name,
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact,
c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.created_at
FROM group_members m
JOIN groups g ON g.group_id = m.group_id
JOIN contact_profiles p ON p.contact_profile_id = m.contact_profile_id
JOIN connections c ON c.group_member_id = m.group_member_id
WHERE g.group_id = ?
ORDER BY c.connection_id DESC
LIMIT 1
|]
(Only groupId)
getUserMember_ :: DB.Connection -> Int64 -> ExceptT StoreError IO GroupMember
getUserMember_ db groupId = ExceptT $ do
userMember
<$> DB.query
db
[sql|
SELECT
m.group_member_id, m.member_id, m.member_role, m.member_status,
m.invited_by, m.contact_id, p.contact_ref, p.display_name
FROM group_members m
JOIN groups g ON g.group_id = m.group_id
JOIN contact_profiles p ON p.contact_profile_id = m.contact_profile_id
WHERE g.group_id = ? AND m.contact_id = ?
|]
(groupId, userContactId)
toContactMember :: (GroupMemberRow :. ConnectionRow) -> (GroupMember, Connection)
toContactMember (memberRow :. connRow) = (toGroupMember memberRow, toConnection connRow)
toGroupMember :: GroupMemberRow -> GroupMember
toGroupMember (groupMemberId, memberId, memberRole, memberStatus, invitedById, memberContactId, contactRef, displayName) =
let memberProfile = Profile {contactRef, displayName}
invitedBy = toInvitedBy userContactId invitedById
in GroupMember {groupMemberId, memberId, memberRole, memberStatus, invitedBy, memberProfile, memberContactId}
userMember :: [GroupMemberRow] -> Either StoreError GroupMember
userMember [memberRow] = Right $ toGroupMember memberRow
userMember _ = Left SEGroupWithoutUser
type GroupMemberRow = (Int64, ByteString, GroupMemberRole, GroupMemberStatus, Maybe Int64, Maybe Int64, ContactRef, Text)
createGroupMember :: (MonadUnliftIO m, MonadError StoreError m) => SQLiteStore -> TVar ChaChaDRG -> User -> Int64 -> Int64 -> GroupMemberRole -> InvitedBy -> ConnId -> m MemberId
createGroupMember st gVar User {userId, userContactId} groupId contactId memberRole invitedBy agentConnId =
liftIOEither . withTransaction st $ \db -> do
let invitedById = fromInvitedBy userContactId invitedBy
memberId <- createWithRandomId gVar $ createMember_ db groupId contactId memberRole GSMemInvited invitedById
groupMemberId <- insertedRowId db
liftIO $ createMemberConnection_ db groupMemberId
pure memberId
where
createMemberConnection_ :: DB.Connection -> Int64 -> IO ()
createMemberConnection_ db groupMemberId =
DB.execute
db
[sql|
INSERT INTO connections
(user_id, agent_conn_id, conn_status, conn_type, group_member_id) VALUES (?,?,?,?,?);
|]
(userId, agentConnId, ConnNew, ConnMember, groupMemberId)
createMember_ :: DB.Connection -> Int64 -> Int64 -> GroupMemberRole -> GroupMemberStatus -> Maybe Int64 -> ByteString -> IO ()
createMember_ db groupId contactId memberRole memberStatus invitedBy memberId =
DB.executeNamed
db
[sql|
INSERT INTO group_members
( group_id, member_id, member_role, member_status, invited_by,
contact_profile_id, contact_id)
VALUES
(:group_id,:member_id,:member_role,:member_status,:invited_by,
(SELECT contact_profile_id FROM contacts WHERE contact_id = :contact_id),
:contact_id)
|]
[ ":group_id" := groupId,
":member_id" := memberId,
":member_role" := memberRole,
":member_status" := memberStatus,
":invited_by" := invitedBy,
":contact_id" := contactId
]
createWithRandomId :: TVar ChaChaDRG -> (ByteString -> IO ()) -> IO (Either StoreError ByteString)
createWithRandomId gVar create = tryCreate 3
where
tryCreate :: Int -> IO (Either StoreError ByteString)
tryCreate 0 = pure $ Left SEUniqueID
tryCreate n = do
id' <- randomId gVar 12
E.try (create id') >>= \case
Right _ -> pure $ Right id'
Left e
| DB.sqlError e == DB.ErrorConstraint -> tryCreate (n - 1)
| otherwise -> pure . Left . SEInternal $ bshow e
randomId :: TVar ChaChaDRG -> Int -> IO ByteString
randomId gVar n = B64.encode <$> (atomically . stateTVar gVar $ randomBytesGenerate n)
data StoreError
= SEDuplicateContactRef
| SEContactNotFound ContactRef
| SEContactNotReady ContactRef
| SEDuplicateGroupRef
| SEGroupNotFound GroupRef
| SEGroupWithoutUser
| SEDuplicateGroupMember
| SEConnectionNotFound ConnId
| SEUniqueID
| SEInternal ByteString
deriving (Show, Exception)
+10 -10
View File
@@ -3,7 +3,6 @@
module Simplex.Chat.Styled
( StyledString (..),
bPlain,
plain,
styleMarkdown,
styleMarkdownText,
@@ -28,12 +27,6 @@ instance Monoid StyledString where mempty = plain ""
instance IsString StyledString where fromString = plain
plain :: String -> StyledString
plain = Styled []
bPlain :: ByteString -> StyledString
bPlain = Styled [] . B.unpack
styleMarkdownText :: Text -> StyledString
styleMarkdownText = styleMarkdown . parseMarkdown
@@ -48,12 +41,19 @@ wrap c s = plain [c] <> s <> plain [c]
class StyledFormat a where
styled :: Format -> a -> StyledString
plain :: a -> StyledString
instance StyledFormat String where styled = Styled . sgr
instance StyledFormat String where
styled = Styled . sgr
plain = Styled []
instance StyledFormat ByteString where styled f = styled f . B.unpack
instance StyledFormat ByteString where
styled f = styled f . B.unpack
plain = Styled [] . B.unpack
instance StyledFormat Text where styled f = styled f . T.unpack
instance StyledFormat Text where
styled f = styled f . T.unpack
plain = Styled [] . T.unpack
sgr :: Format -> [SGR]
sgr = \case
+99 -10
View File
@@ -1,6 +1,7 @@
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
module Simplex.Chat.Types where
@@ -8,10 +9,15 @@ module Simplex.Chat.Types where
import Data.Aeson (FromJSON, ToJSON)
import qualified Data.Aeson as J
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import Data.Int (Int64)
import Data.Text (Text)
import Data.Time.Clock (UTCTime)
import Database.SQLite.Simple.FromField (FromField (..))
import Data.Typeable (Typeable)
import Database.SQLite.Simple (ResultError (..), SQLData (..))
import Database.SQLite.Simple.FromField (FieldParser, FromField (..), returnError)
import Database.SQLite.Simple.Internal (Field (..))
import Database.SQLite.Simple.Ok (Ok (Ok))
import Database.SQLite.Simple.ToField (ToField (..))
import GHC.Generics
import Simplex.Messaging.Agent.Protocol (ConnId)
@@ -19,6 +25,7 @@ import Simplex.Messaging.Agent.Store.SQLite (fromTextField_)
data User = User
{ userId :: UserId,
userContactId :: Int64,
localContactRef :: ContactRef,
profile :: Profile,
activeUser :: Bool
@@ -26,23 +33,27 @@ data User = User
type UserId = Int64
data Contact
= Contact
{ contactId :: Int64,
localContactRef :: ContactRef,
profile :: Profile,
activeConn :: Connection
}
| NewContact {activeConn :: Connection}
data Contact = Contact
{ contactId :: Int64,
localContactRef :: ContactRef,
profile :: Profile,
activeConn :: Connection
}
deriving (Eq, Show)
contactConnId :: Contact -> ConnId
contactConnId Contact {activeConn = Connection {agentConnId}} = agentConnId
type ContactRef = Text
type GroupRef = Text
data Group = Group
{ groupId :: Int64,
localGroupRef :: Text
localGroupRef :: Text,
groupProfile :: GroupProfile,
members :: [(GroupMember, Connection)],
membership :: GroupMember
}
deriving (Eq, Show)
@@ -66,6 +77,84 @@ instance ToJSON GroupProfile where toEncoding = J.genericToEncoding J.defaultOpt
instance FromJSON GroupProfile
data GroupMember = GroupMember
{ groupMemberId :: Int64,
memberId :: MemberId,
memberRole :: GroupMemberRole,
memberStatus :: GroupMemberStatus,
invitedBy :: InvitedBy,
memberProfile :: Profile,
memberContactId :: Maybe Int64
}
deriving (Eq, Show)
type MemberId = ByteString
data InvitedBy = IBContact Int64 | IBUser | IBUnknown
deriving (Eq, Show)
toInvitedBy :: Int64 -> Maybe Int64 -> InvitedBy
toInvitedBy userCtId (Just ctId)
| userCtId == ctId = IBUser
| otherwise = IBContact ctId
toInvitedBy _ Nothing = IBUnknown
fromInvitedBy :: Int64 -> InvitedBy -> Maybe Int64
fromInvitedBy userCtId = \case
IBUnknown -> Nothing
IBContact ctId -> Just ctId
IBUser -> Just userCtId
data GroupMemberRole = GRMember | GRAdmin | GROwner
deriving (Eq, Show, Ord)
instance FromField GroupMemberRole where fromField = fromBlobField_ toMemberRole
instance ToField GroupMemberRole where toField = toField . serializeMemberRole
toMemberRole :: ByteString -> Either String GroupMemberRole
toMemberRole = \case
"owner" -> Right GROwner
"admin" -> Right GRAdmin
"member" -> Right GRMember
r -> Left $ "invalid group member role " <> B.unpack r
serializeMemberRole :: GroupMemberRole -> ByteString
serializeMemberRole = \case
GROwner -> "owner"
GRAdmin -> "admin"
GRMember -> "member"
fromBlobField_ :: Typeable k => (ByteString -> Either String k) -> FieldParser k
fromBlobField_ p = \case
f@(Field (SQLBlob b) _) ->
case p b of
Right k -> Ok k
Left e -> returnError ConversionFailed f ("couldn't parse field: " ++ e)
f -> returnError ConversionFailed f "expecting SQLBlob column type"
data GroupMemberStatus = GSMemInvited | GSMemAccepted | GSMemConnected | GSMemReady
deriving (Eq, Show)
instance FromField GroupMemberStatus where fromField = fromTextField_ memberStatusT
instance ToField GroupMemberStatus where toField = toField . serializeMemberStatus
memberStatusT :: Text -> Maybe GroupMemberStatus
memberStatusT = \case
"invited" -> Just GSMemInvited
"accepted" -> Just GSMemAccepted
"connected" -> Just GSMemConnected
"ready" -> Just GSMemReady
_ -> Nothing
serializeMemberStatus :: GroupMemberStatus -> Text
serializeMemberStatus = \case
GSMemInvited -> "invited"
GSMemAccepted -> "accepted"
GSMemConnected -> "connected"
GSMemReady -> "ready"
data Connection = Connection
{ connId :: Int64,
agentConnId :: ConnId,
+18 -6
View File
@@ -1,6 +1,8 @@
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
module Simplex.Chat.View
@@ -12,6 +14,7 @@ module Simplex.Chat.View
showContactDisconnected,
showReceivedMessage,
showSentMessage,
showGroupCreated,
safeDecodeUtf8,
)
where
@@ -26,6 +29,7 @@ import Data.Time.Format (defaultTimeLocale, formatTime)
import Data.Time.LocalTime (TimeZone, ZonedTime, getCurrentTimeZone, getZonedTime, localDay, localTimeOfDay, timeOfDayToTime, utcToLocalTime, zonedTimeToLocalTime)
import Simplex.Chat.Controller
import Simplex.Chat.Markdown
import Simplex.Chat.Store (StoreError (..))
import Simplex.Chat.Styled
import Simplex.Chat.Terminal (printToTerminal)
import Simplex.Chat.Types
@@ -56,11 +60,14 @@ showReceivedMessage c utcTime msg mOk = printToView =<< liftIO (receivedMessage
showSentMessage :: ChatReader m => ContactRef -> ByteString -> m ()
showSentMessage c msg = printToView =<< liftIO (sentMessage c msg)
showGroupCreated :: ChatReader m => GroupProfile -> m ()
showGroupCreated = printToView . groupCreated
invitation :: SMPQueueInfo -> [StyledString]
invitation qInfo =
[ "pass this invitation to your contact (via another channel): ",
"",
(bPlain . serializeSmpQueueInfo) qInfo,
(plain . serializeSmpQueueInfo) qInfo,
"",
"and ask them to connect: /c <name_for_you> <invitation_above>"
]
@@ -74,6 +81,9 @@ contactConnected c = [ttyContact c <> " is connected"]
contactDisconnected :: ContactRef -> [StyledString]
contactDisconnected c = ["disconnected from " <> ttyContact c <> " - restart chat"]
groupCreated :: GroupProfile -> [StyledString]
groupCreated GroupProfile {groupRef, displayName} = ["group " <> ttyGroup groupRef <> " (" <> plain displayName <> ") is created"]
receivedMessage :: ContactRef -> UTCTime -> Text -> MsgIntegrity -> IO [StyledString]
receivedMessage c utcTime msg mOk = do
t <- formatUTCTime <$> getCurrentTimeZone <*> getZonedTime
@@ -114,9 +124,11 @@ msgPlain = map styleMarkdownText . T.lines
chatError :: ChatError -> [StyledString]
chatError = \case
ChatErrorContact e -> case e of
CENotFound c -> ["no contact " <> ttyContact c]
CEProfile s -> ["invalid profile: " <> plain s]
ChatErrorStore err -> case err of
SEContactNotFound c -> ["no contact " <> ttyContact c]
SEContactNotReady c -> ["contact " <> ttyContact c <> " is not active yet"]
SEDuplicateGroupRef -> ["group with this alias already exists"]
e -> ["chat db error: " <> plain (show e)]
ChatErrorAgent err -> case err of
-- CONN e -> case e of
-- -- TODO replace with ChatErrorContact errors, these errors should never happen
@@ -138,8 +150,8 @@ ttyToContact c = styled (Colored Cyan) $ "@" <> c <> " "
ttyFromContact :: ContactRef -> StyledString
ttyFromContact c = styled (Colored Yellow) $ c <> "> "
-- ttyGroup :: Group -> StyledString
-- ttyGroup (Group g) = styled (Colored Blue) $ "#" <> g
ttyGroup :: GroupRef -> StyledString
ttyGroup g = styled (Colored Blue) $ "#" <> g
-- ttyFromGroup :: Group -> Contact -> StyledString
-- ttyFromGroup (Group g) (Contact a) = styled (Colored Yellow) $ "#" <> g <> " " <> a <> "> "
+11 -5
View File
@@ -69,13 +69,15 @@ refMsgHash = 16*16(OCTET) ; SHA256 of agent message body
### Group protocol
A -> B: invite to group - `MSG: x.grp.inv G_INV_ID,G_MEM_ID_B,G_MEM_ROLE x.json:NNN <group_profile>`
#### Add group member
A -> B: invite to group - `MSG: x.grp.inv G_MEM_ID_A,G_MEM_ROLE_A,G_MEM_ID_B,G_MEM_ROLE_B,<invitation> x.json:NNN <group_profile>`
user B confirms
B -> A: join group - `MSG: x.grp.acpt G_INV_ID,<invitation>`
A -> Bg: establish group connection (A: JOIN, B: LET)
A -> group (including B)): announce group member: `MSG: N x.grp.mem.new G_MEM_ID_B,G_MEM_ROLE x.json:NNN <B_profile>`
B -> A: establish group connection (B: JOIN, A: LET)
B -> Ag: join group - `in SMP confirmation: x.grp.acpt G_MEM_ID_B`
A -> group (including B)): announce group member: `MSG: N x.grp.mem.new G_MEM_ID_B,G_MEM_ROLE_B x.json:NNN <B_profile>`
subsequent messages between A and B are via group connection
A -> Bg: intro member - `MSG: x.grp.mem.intro G_MEM_ID_M,G_MEM_ROLE x.json:NNN <M_profile>`
A -> Bg: intro member - `MSG: x.grp.mem.intro G_MEM_ID_M,G_MEM_ROLE_M x.json:NNN <M_profile>`
B -> Ag: inv for mem - `MSG: x.grp.mem.inv G_MEM_ID_M,<gr_invitation>,<dm_invitation>,<probe>`
M is an existing member, messages are via group connection
A -> Mg: fwd inv - `MSG: x.grp.mem.fwd G_MEM_ID_B,<gr_invitation>,<dm_invitation>,<probe>`
@@ -91,3 +93,7 @@ M -> Ag: connected to M: `MSG: x.grp.mem.con G_MEM_ID_B`
once all members connected
A -> group: `MSG: N x.grp.mem.ok G_MEM_ID_B`
#### Send group message
`MSG: N x.msg.new G_MEM_ROLE,<invitation> x.json:NNN <group_profile>`