core chat groups protocol for adding members (#78)

* add category and local display name to group members, extend member status

* additional chat commands, serialization

* parse all chat messages

* draft group protocol implementation

* group protocol: connect new member to existing members (TODO fix race condition with contact connection)

* send/receive group messages (race condition still there - the 3rd member cannot send either group or direct messages to the 2nd member - CONN SIMPLEX)

* send x.grp.mem.info and x.ok in SMP confirmation

* fix host user adding new member, update simplexmq to fix sqlite concurrency, remove logs, make # optional in chat commands

* more precise view messages about members joining and connecting

* track connection status; only send messages to active members (TODO change to current members); group name autocomplete after joining the group

* track via which group the contact was added; show only one message when a contact fully connected; group tests

* test sending messages to the new direct contacts created via the group

* update simplexmq to include .cabal file

* remove unused import
This commit is contained in:
Evgeny Poberezkin
2021-07-24 10:26:28 +01:00
committed by GitHub
parent 94f89ed8f7
commit 189cd7e09d
14 changed files with 1262 additions and 347 deletions
+3 -2
View File
@@ -41,11 +41,12 @@ data ChatError
deriving (Show, Exception)
data ChatErrorType
= CEGroupRole
= CEGroupUserRole
| CEGroupContactRole ContactName
| CEGroupDuplicateMember ContactName
| CEGroupDuplicateMemberId
| CEGroupNotJoined GroupName
| CEGroupMemberNotReady
| CEGroupMemberNotActive
| CEGroupInternal String
deriving (Show, Exception)
+1 -1
View File
@@ -81,7 +81,7 @@ updateTermState ac tw (key, ms) ts@TerminalState {inputString = s, inputPosition
contactPrefix = case ac of
ActiveNone -> ""
ActiveC c -> "@" <> T.unpack c <> " "
-- ActiveG (Group g) -> "#" <> B.unpack g <> " "
ActiveG g -> "#" <> T.unpack g <> " "
backDeleteChar
| p == 0 || null s = ts
| p >= length s = ts' (init s, length s - 1)
-1
View File
@@ -5,7 +5,6 @@
module Simplex.Chat.Notification (Notification (..), initializeNotifications) where
import Control.Monad (void)
import Data.Char (toLower)
import Data.List (isInfixOf)
import Data.Text (Text)
import qualified Data.Text as T
+123 -83
View File
@@ -29,33 +29,43 @@ import Simplex.Messaging.Parsers (parseAll)
import Simplex.Messaging.Util (bshow)
data ChatDirection (p :: AParty) where
ReceivedDirectMessage :: ConnContact -> ChatDirection 'Agent
ReceivedDMConnection :: Connection -> ChatDirection 'Agent
ReceivedDMContact :: Contact -> ChatDirection 'Agent
SentDirectMessage :: Contact -> ChatDirection 'Client
ReceivedGroupMessage :: GroupName -> GroupMember -> ChatDirection 'Agent
ReceivedGroupMessage :: Connection -> GroupName -> GroupMember -> ChatDirection 'Agent
SentGroupMessage :: GroupName -> 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 MsgContent
| XInfo Profile
| XGrpInv GroupInvitation
| XGrpAcpt MemberId
| XGrpMemNew MemberId GroupMemberRole Profile
| XGrpMemIntro MemberId GroupMemberRole Profile
| XGrpMemNew MemberInfo
| XGrpMemIntro MemberInfo
| XGrpMemInv MemberId IntroInvitation
| XGrpMemFwd MemberInfo IntroInvitation
| XGrpMemInfo MemberId Profile
| XGrpMemCon MemberId
| XGrpMemConAll MemberId
| XInfoProbe ByteString
| XInfoProbeCheck MemberId ByteString
| XInfoProbeOk MemberId ByteString
| XOk
deriving (Eq, Show)
data MessageType = MTText | MTImage deriving (Eq, Show)
data MsgContent = MsgContent
{ messageType :: MessageType,
files :: [(ContentType, Int)],
content :: [MsgContentBody]
}
deriving (Eq, Show)
toMsgType :: ByteString -> Either String MessageType
toMsgType = \case
"c.text" -> Right MTText
@@ -77,118 +87,148 @@ data ChatMessage = ChatMessage
toChatMessage :: RawChatMessage -> Either String ChatMessage
toChatMessage RawChatMessage {chatMsgId, chatMsgEvent, chatMsgParams, chatMsgBody} = do
(chatDAG, body) <- getDAG <$> mapM toMsgBodyContent chatMsgBody
case chatMsgEvent of
"x.msg.new" -> case chatMsgParams of
mt : rawFiles -> do
t <- toMsgType mt
files <- mapM (toContentInfo <=< parseAll contentInfoP) rawFiles
let msg = XMsgNew {messageType = t, files, content = body}
pure ChatMessage {chatMsgId, chatMsgEvent = msg, chatDAG}
[] -> 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}
_ -> Left "x.info expects no parameters"
"x.grp.inv" -> case chatMsgParams of
[fromMemId, fromRole, memId, role, qInfo] -> do
fromMember <- (,) <$> B64.decode fromMemId <*> toMemberRole fromRole
invitedMember <- (,) <$> B64.decode memId <*> toMemberRole role
inv <- GroupInvitation fromMember invitedMember <$> parseAll smpQueueInfoP qInfo <*> getJSON body
pure ChatMessage {chatMsgId, chatMsgEvent = XGrpInv inv, chatDAG}
_ -> Left "x.grp.inv expects 5 parameters"
"x.grp.acpt" -> case chatMsgParams of
[memId] -> do
msg <- XGrpAcpt <$> B64.decode memId
pure ChatMessage {chatMsgId, chatMsgEvent = msg, chatDAG}
_ -> 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
let chatMsg msg = pure ChatMessage {chatMsgId, chatMsgEvent = msg, chatDAG}
case (chatMsgEvent, chatMsgParams) of
("x.msg.new", mt : rawFiles) -> do
t <- toMsgType mt
files <- mapM (toContentInfo <=< parseAll contentInfoP) rawFiles
chatMsg . XMsgNew $ MsgContent {messageType = t, files, content = body}
("x.info", []) -> do
profile <- getJSON body
chatMsg $ XInfo profile
("x.grp.inv", [fromMemId, fromRole, memId, role, qInfo]) -> do
fromMem <- (,) <$> B64.decode fromMemId <*> toMemberRole fromRole
invitedMem <- (,) <$> B64.decode memId <*> toMemberRole role
groupQInfo <- parseAll smpQueueInfoP qInfo
profile <- getJSON body
chatMsg . XGrpInv $ GroupInvitation fromMem invitedMem groupQInfo profile
("x.grp.acpt", [memId]) ->
chatMsg . XGrpAcpt =<< B64.decode memId
("x.grp.mem.new", [memId, role]) -> do
chatMsg . XGrpMemNew =<< toMemberInfo memId role body
("x.grp.mem.intro", [memId, role]) ->
chatMsg . XGrpMemIntro =<< toMemberInfo memId role body
("x.grp.mem.inv", [memId, groupQInfo, directQInfo]) ->
chatMsg =<< (XGrpMemInv <$> B64.decode memId <*> toIntroInv groupQInfo directQInfo)
("x.grp.mem.fwd", [memId, role, groupQInfo, directQInfo]) -> do
chatMsg =<< (XGrpMemFwd <$> toMemberInfo memId role body <*> toIntroInv groupQInfo directQInfo)
("x.grp.mem.info", [memId]) ->
chatMsg =<< (XGrpMemInfo <$> B64.decode memId <*> getJSON body)
("x.grp.mem.con", [memId]) ->
chatMsg . XGrpMemCon =<< B64.decode memId
("x.grp.mem.con.all", [memId]) ->
chatMsg . XGrpMemConAll =<< B64.decode memId
("x.info.probe", [probe]) -> do
chatMsg . XInfoProbe =<< B64.decode probe
("x.info.probe.check", [memId, probeHash]) -> do
chatMsg =<< (XInfoProbeCheck <$> B64.decode memId <*> B64.decode probeHash)
("x.info.probe.ok", [memId, probe]) -> do
chatMsg =<< (XInfoProbeOk <$> B64.decode memId <*> B64.decode probe)
("x.ok", []) ->
chatMsg XOk
_ -> Left $ "bad syntax or unsupported event " <> B.unpack chatMsgEvent
where
getDAG :: [MsgBodyContent] -> (Maybe ByteString, [MsgBodyContent])
getDAG :: [MsgContentBody] -> (Maybe ByteString, [MsgContentBody])
getDAG body = case break (isContentType SimplexDAG) body of
(b, MsgBodyContent SimplexDAG dag : a) -> (Just dag, b <> a)
(b, MsgContentBody 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"
toMemberInfo :: ByteString -> ByteString -> [MsgContentBody] -> Either String MemberInfo
toMemberInfo memId role body = MemberInfo <$> B64.decode memId <*> toMemberRole role <*> getJSON body
toIntroInv :: ByteString -> ByteString -> Either String IntroInvitation
toIntroInv groupQInfo directQInfo = IntroInvitation <$> parseAll smpQueueInfoP groupQInfo <*> parseAll smpQueueInfoP directQInfo
toContentInfo :: (RawContentType, Int) -> Either String (ContentType, Int)
toContentInfo (rawType, size) = (,size) <$> toContentType rawType
getJSON :: FromJSON a => [MsgBodyContent] -> Either String a
getJSON :: FromJSON a => [MsgContentBody] -> Either String a
getJSON = J.eitherDecodeStrict' <=< getSimplexContentType XCJson
isContentType :: ContentType -> MsgBodyContent -> Bool
isContentType t MsgBodyContent {contentType = t'} = t == t'
isContentType :: ContentType -> MsgContentBody -> Bool
isContentType t MsgContentBody {contentType = t'} = t == t'
isSimplexContentType :: XContentType -> MsgBodyContent -> Bool
isSimplexContentType :: XContentType -> MsgContentBody -> Bool
isSimplexContentType = isContentType . SimplexContentType
getContentType :: ContentType -> [MsgBodyContent] -> Either String ByteString
getContentType :: ContentType -> [MsgContentBody] -> Either String ByteString
getContentType t body = case find (isContentType t) body of
Just MsgBodyContent {contentData} -> Right contentData
Just MsgContentBody {contentData} -> Right contentData
Nothing -> Left "no required content type"
getSimplexContentType :: XContentType -> [MsgBodyContent] -> Either String ByteString
getSimplexContentType :: XContentType -> [MsgContentBody] -> Either String ByteString
getSimplexContentType = getContentType . SimplexContentType
rawChatMessage :: ChatMessage -> RawChatMessage
rawChatMessage ChatMessage {chatMsgId, chatMsgEvent, chatDAG} =
case chatMsgEvent of
XMsgNew {messageType = t, files, content} ->
XMsgNew MsgContent {messageType = t, files, content} ->
let rawFiles = map (serializeContentInfo . rawContentInfo) files
chatMsgParams = rawMsgType t : rawFiles
chatMsgBody = rawWithDAG content
in RawChatMessage {chatMsgId, chatMsgEvent = "x.msg.new", chatMsgParams, chatMsgBody}
in rawMsg "x.msg.new" (rawMsgType t : rawFiles) content
XInfo profile ->
let chatMsgBody = rawWithDAG [jsonBody profile]
in RawChatMessage {chatMsgId, chatMsgEvent = "x.info", chatMsgParams = [], chatMsgBody}
rawMsg "x.info" [] [jsonBody profile]
XGrpInv (GroupInvitation (fromMemId, fromRole) (memId, role) qInfo groupProfile) ->
let chatMsgParams =
let params =
[ 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}
in rawMsg "x.grp.inv" params [jsonBody groupProfile]
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]
chatMsgBody = rawWithDAG [jsonBody profile]
in RawChatMessage {chatMsgId, chatMsgEvent = "x.grp.mem.new", chatMsgParams, chatMsgBody}
XGrpMemIntro memId role profile ->
let chatMsgParams = [B64.encode memId, serializeMemberRole role]
chatMsgBody = rawWithDAG [jsonBody profile]
in RawChatMessage {chatMsgId, chatMsgEvent = "x.grp.mem.intro", chatMsgParams, chatMsgBody}
rawMsg "x.grp.acpt" [B64.encode memId] []
XGrpMemNew (MemberInfo memId role profile) ->
let params = [B64.encode memId, serializeMemberRole role]
in rawMsg "x.grp.mem.new" params [jsonBody profile]
XGrpMemIntro (MemberInfo memId role profile) ->
rawMsg "x.grp.mem.intro" [B64.encode memId, serializeMemberRole role] [jsonBody profile]
XGrpMemInv memId IntroInvitation {groupQInfo, directQInfo} ->
let params = [B64.encode memId, serializeSmpQueueInfo groupQInfo, serializeSmpQueueInfo directQInfo]
in rawMsg "x.grp.mem.inv" params []
XGrpMemFwd (MemberInfo memId role profile) IntroInvitation {groupQInfo, directQInfo} ->
let params =
[ B64.encode memId,
serializeMemberRole role,
serializeSmpQueueInfo groupQInfo,
serializeSmpQueueInfo directQInfo
]
in rawMsg "x.grp.mem.fwd" params [jsonBody profile]
XGrpMemInfo memId profile ->
rawMsg "x.grp.mem.info" [B64.encode memId] [jsonBody profile]
XGrpMemCon memId ->
rawMsg "x.grp.mem.con" [B64.encode memId] []
XGrpMemConAll memId ->
rawMsg "x.grp.mem.con.all" [B64.encode memId] []
XInfoProbe probe ->
rawMsg "x.info.probe" [B64.encode probe] []
XInfoProbeCheck memId probeHash ->
rawMsg "x.info.probe.check" [B64.encode memId, B64.encode probeHash] []
XInfoProbeOk memId probe ->
rawMsg "x.info.probe.ok" [B64.encode memId, B64.encode probe] []
XOk ->
rawMsg "x.ok" [] []
where
rawMsg :: ByteString -> [ByteString] -> [MsgContentBody] -> RawChatMessage
rawMsg event chatMsgParams body =
RawChatMessage {chatMsgId, chatMsgEvent = event, chatMsgParams, chatMsgBody = rawWithDAG body}
rawContentInfo :: (ContentType, Int) -> (RawContentType, Int)
rawContentInfo (t, size) = (rawContentType t, size)
jsonBody :: ToJSON a => a -> MsgBodyContent
jsonBody :: ToJSON a => a -> MsgContentBody
jsonBody x =
let json = LB.toStrict $ J.encode x
in MsgBodyContent {contentType = SimplexContentType XCJson, contentData = json}
rawWithDAG :: [MsgBodyContent] -> [RawMsgBodyContent]
in MsgContentBody {contentType = SimplexContentType XCJson, contentData = json}
rawWithDAG :: [MsgContentBody] -> [RawMsgBodyContent]
rawWithDAG body = map rawMsgBodyContent $ case chatDAG of
Nothing -> body
Just dag -> MsgBodyContent {contentType = SimplexDAG, contentData = dag} : body
Just dag -> MsgContentBody {contentType = SimplexDAG, contentData = dag} : body
toMsgBodyContent :: RawMsgBodyContent -> Either String MsgBodyContent
toMsgBodyContent :: RawMsgBodyContent -> Either String MsgContentBody
toMsgBodyContent RawMsgBodyContent {contentType, contentData} = do
cType <- toContentType contentType
pure MsgBodyContent {contentType = cType, contentData}
pure MsgContentBody {contentType = cType, contentData}
rawMsgBodyContent :: MsgBodyContent -> RawMsgBodyContent
rawMsgBodyContent MsgBodyContent {contentType = t, contentData} =
rawMsgBodyContent :: MsgContentBody -> RawMsgBodyContent
rawMsgBodyContent MsgContentBody {contentType = t, contentData} =
RawMsgBodyContent {contentType = rawContentType t, contentData}
data MsgBodyContent = MsgBodyContent
data MsgContentBody = MsgContentBody
{ contentType :: ContentType,
contentData :: ByteString
}
+403 -79
View File
@@ -25,13 +25,23 @@ module Simplex.Chat.Store
getContact,
getContactConnections,
getConnectionChatDirection,
updateConnectionStatus,
createNewGroup,
createGroupInvitation,
getGroup,
getGroupInvitation,
createGroupMember,
createContactGroupMember,
createMemberConnection,
updateGroupMemberStatus,
createNewGroupMember,
createIntroductions,
updateIntroStatus,
saveIntroInvitation,
createIntroReMember,
createIntroToMemberContact,
saveMemberInvitation,
getViaGroupMember,
getViaGroupContact,
)
where
@@ -51,7 +61,7 @@ import Data.Maybe (listToMaybe)
import Data.Text (Text)
import qualified Data.Text as T
import Data.Text.Encoding (decodeUtf8)
import Data.Time.Clock (UTCTime)
import Data.Time.Clock (UTCTime, getCurrentTime)
import Database.SQLite.Simple (NamedParam (..), Only (..), SQLError, (:.) (..))
import qualified Database.SQLite.Simple as DB
import Database.SQLite.Simple.QQ (sql)
@@ -129,23 +139,36 @@ setActiveUser st userId = do
createDirectConnection :: MonadUnliftIO m => SQLiteStore -> UserId -> ConnId -> m ()
createDirectConnection st userId agentConnId =
liftIO . withTransaction st $ \db ->
DB.execute
db
[sql|
INSERT INTO connections
(user_id, agent_conn_id, conn_status, conn_type) VALUES (?,?,?,?);
|]
(userId, agentConnId, ConnNew, ConnContact)
void $ createConnection_ db userId agentConnId Nothing 0
createConnection_ :: DB.Connection -> UserId -> ConnId -> Maybe Int64 -> Int -> IO Connection
createConnection_ db userId agentConnId viaContact connLevel = do
createdAt <- getCurrentTime
DB.execute
db
[sql|
INSERT INTO connections
(user_id, agent_conn_id, conn_status, conn_type, via_contact, conn_level, created_at) VALUES (?,?,?,?,?,?,?);
|]
(userId, agentConnId, ConnNew, ConnContact, viaContact, connLevel, createdAt)
connId <- insertedRowId db
pure Connection {connId, agentConnId, connType = ConnContact, entityId = Nothing, viaContact, connLevel, connStatus = ConnNew, createdAt}
createDirectContact :: StoreMonad m => SQLiteStore -> UserId -> Connection -> Profile -> m ()
createDirectContact st userId Connection {connId} Profile {displayName, fullName} =
liftIOEither . withTransaction st $ \db ->
withLocalDisplayName db userId displayName $ \localDisplayName' -> do
DB.execute db "INSERT INTO contact_profiles (display_name, full_name) VALUES (?, ?)" (displayName, fullName)
profileId <- insertedRowId db
DB.execute db "INSERT INTO contacts (contact_profile_id, local_display_name, user_id) VALUES (?, ?, ?)" (profileId, localDisplayName', userId)
contactId <- insertedRowId db
DB.execute db "UPDATE connections SET contact_id = ? WHERE connection_id = ?" (contactId, connId)
createDirectContact st userId Connection {connId} profile =
void $
liftIOEither . withTransaction st $ \db ->
createContact_ db userId connId profile Nothing
createContact_ :: DB.Connection -> UserId -> Int64 -> Profile -> Maybe Int64 -> IO (Either StoreError (Text, Int64, Int64))
createContact_ db userId connId Profile {displayName, fullName} viaGroup =
withLocalDisplayName db userId displayName $ \ldn -> do
DB.execute db "INSERT INTO contact_profiles (display_name, full_name) VALUES (?, ?)" (displayName, fullName)
profileId <- insertedRowId db
DB.execute db "INSERT INTO contacts (contact_profile_id, local_display_name, user_id, via_group) VALUES (?,?,?,?)" (profileId, ldn, userId, viaGroup)
contactId <- insertedRowId db
DB.execute db "UPDATE connections SET contact_id = ? WHERE connection_id = ?" (contactId, connId)
pure (ldn, contactId, profileId)
deleteContact :: MonadUnliftIO m => SQLiteStore -> UserId -> ContactName -> m ()
deleteContact st userId displayName =
@@ -178,25 +201,27 @@ deleteContact st userId displayName =
-- TODO return the last connection that is ready, not any last connection
-- requires updating connection status
getContact ::
StoreMonad m => SQLiteStore -> UserId -> ContactName -> m Contact
getContact :: StoreMonad m => SQLiteStore -> UserId -> ContactName -> m Contact
-- TODO merge contact and connection?
getContact st userId localDisplayName =
liftIOEither . withTransaction st $ \db -> runExceptT $ do
c@Contact {contactId} <- getContact_ db
activeConn <- getConnection_ db contactId
pure $ (c :: Contact) {activeConn}
where
getContact_ :: DB.Connection -> ExceptT StoreError IO Contact
getContact_ db = ExceptT $ do
toContact
<$> DB.queryNamed
db
[sql|
SELECT c.contact_id, p.display_name, p.full_name
SELECT c.contact_id, p.display_name, p.full_name, c.via_group
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_display_name = :local_display_name AND c.is_user = :is_user
|]
[":user_id" := userId, ":local_display_name" := localDisplayName, ":is_user" := False]
getConnection_ :: DB.Connection -> Int64 -> ExceptT StoreError IO Connection
getConnection_ db contactId = ExceptT $ do
connection
<$> DB.queryNamed
@@ -210,10 +235,12 @@ getContact st userId localDisplayName =
LIMIT 1
|]
[":user_id" := userId, ":contact_id" := contactId]
toContact [(contactId, displayName, fullName)] =
toContact :: [(Int64, Text, Text, Maybe Int64)] -> Either StoreError Contact
toContact [(contactId, displayName, fullName, viaGroup)] =
let profile = Profile {displayName, fullName}
in Right Contact {contactId, localDisplayName, profile, activeConn = undefined}
in Right Contact {contactId, localDisplayName, profile, activeConn = undefined, viaGroup}
toContact _ = Left $ SEContactNotFound localDisplayName
connection :: [ConnectionRow] -> Either StoreError Connection
connection (connRow : _) = Right $ toConnection connRow
connection _ = Left $ SEContactNotReady localDisplayName
@@ -263,11 +290,11 @@ getConnectionChatDirection st User {userId, userContactId} agentConnId =
ConnMember ->
case entityId of
Nothing -> throwError $ SEInternal "group member without connection"
Just groupMemberId -> uncurry ReceivedGroupMessage <$> getGroupAndMember_ db groupMemberId
Just groupMemberId -> uncurry (ReceivedGroupMessage c) <$> getGroupAndMember_ db groupMemberId c
ConnContact ->
ReceivedDirectMessage <$> case entityId of
Nothing -> pure $ CConnection c
Just contactId -> getContact_ db contactId c
case entityId of
Nothing -> pure $ ReceivedDMConnection c
Just contactId -> ReceivedDMContact <$> getContact_ db contactId c
where
getConnection_ :: DB.Connection -> ExceptT StoreError IO Connection
getConnection_ db = ExceptT $ do
@@ -284,42 +311,49 @@ getConnectionChatDirection st User {userId, userContactId} agentConnId =
connection :: [ConnectionRow] -> Either StoreError Connection
connection (connRow : _) = Right $ toConnection connRow
connection _ = Left $ SEConnectionNotFound agentConnId
getContact_ :: DB.Connection -> Int64 -> Connection -> ExceptT StoreError IO ConnContact
getContact_ :: DB.Connection -> Int64 -> Connection -> ExceptT StoreError IO Contact
getContact_ db contactId c = ExceptT $ do
toContact contactId c
<$> DB.query
db
[sql|
SELECT c.local_display_name, p.display_name, p.full_name
SELECT c.local_display_name, p.display_name, p.full_name, c.via_group
FROM contacts c
JOIN contact_profiles p ON c.contact_profile_id = p.contact_profile_id
WHERE c.user_id = ? AND c.contact_id = ?
|]
(userId, contactId)
toContact :: Int64 -> Connection -> [(ContactName, Text, Text)] -> Either StoreError ConnContact
toContact contactId c [(localDisplayName, displayName, fullName)] =
toContact :: Int64 -> Connection -> [(ContactName, Text, Text, Maybe Int64)] -> Either StoreError Contact
toContact contactId activeConn [(localDisplayName, displayName, fullName, viaGroup)] =
let profile = Profile {displayName, fullName}
in Right $ CContact Contact {contactId, localDisplayName, profile, activeConn = c}
in Right $ Contact {contactId, localDisplayName, profile, activeConn, viaGroup}
toContact _ _ _ = Left $ SEInternal "referenced contact not found"
getGroupAndMember_ :: DB.Connection -> Int64 -> ExceptT StoreError IO (GroupName, GroupMember)
getGroupAndMember_ db groupMemberId = ExceptT $ do
toGroupAndMember
getGroupAndMember_ :: DB.Connection -> Int64 -> Connection -> ExceptT StoreError IO (GroupName, GroupMember)
getGroupAndMember_ db groupMemberId c = ExceptT $ do
toGroupAndMember c
<$> DB.query
db
[sql|
SELECT
g.local_display_name,
m.group_member_id, m.member_id, m.member_role, m.member_status,
m.invited_by, m.contact_id, p.display_name, p.full_name
m.group_member_id, m.member_id, m.member_role, m.member_category, m.member_status,
m.invited_by, m.local_display_name, m.contact_id, p.display_name, p.full_name
FROM group_members m
JOIN contact_profiles p ON p.contact_profile_id = m.contact_profile_id
JOIN groups g ON g.group_id = m.group_id
WHERE m.group_member_id = ?
|]
(Only groupMemberId)
toGroupAndMember :: [Only GroupName :. GroupMemberRow] -> Either StoreError (GroupName, GroupMember)
toGroupAndMember [Only groupName :. memberRow] = Right (groupName, toGroupMember userContactId memberRow)
toGroupAndMember _ = Left $ SEInternal "referenced group member not found"
toGroupAndMember :: Connection -> [Only GroupName :. GroupMemberRow] -> Either StoreError (GroupName, GroupMember)
toGroupAndMember c [Only groupName :. memberRow] =
let member = toGroupMember userContactId memberRow
in Right (groupName, (member :: GroupMember) {activeConn = Just c})
toGroupAndMember _ _ = Left $ SEInternal "referenced group member not found"
updateConnectionStatus :: MonadUnliftIO m => SQLiteStore -> Connection -> ConnStatus -> m ()
updateConnectionStatus st Connection {connId} connStatus =
liftIO . withTransaction st $ \db ->
DB.execute db "UPDATE connections SET conn_status = ? WHERE connection_id = ?" (connStatus, connId)
-- | creates completely new group with a single member - the current user
createNewGroup :: StoreMonad m => SQLiteStore -> TVar ChaChaDRG -> User -> GroupProfile -> m Group
@@ -333,7 +367,7 @@ createNewGroup st gVar user groupProfile =
DB.execute db "INSERT INTO groups (local_display_name, user_id, group_profile_id) VALUES (?, ?, ?)" (displayName, uId, profileId)
groupId <- insertedRowId db
memberId <- randomId gVar 12
membership <- createContactMember_ db user groupId user (memberId, GROwner) GSMemFull IBUser
membership <- createContactMember_ db user groupId user (memberId, GROwner) GCUserMember GSMemCreator IBUser
pure $ Right Group {groupId, localDisplayName = displayName, groupProfile, members = [], membership}
-- | creates a new group record for the group the current user was invited to
@@ -348,9 +382,9 @@ createGroupInvitation st user contact GroupInvitation {fromMember, invitedMember
profileId <- insertedRowId db
DB.execute db "INSERT INTO groups (group_profile_id, local_display_name, inv_queue_info, user_id) VALUES (?, ?, ?, ?)" (profileId, localDisplayName, queueInfo, uId)
groupId <- insertedRowId db
member <- createContactMember_ db user groupId contact fromMember GSMemFull IBUnknown
membership <- createContactMember_ db user groupId user invitedMember GSMemInvited (IBContact $ contactId contact)
pure Group {groupId, localDisplayName, groupProfile, members = [(member, Nothing)], membership}
member <- createContactMember_ db user groupId contact fromMember GCHostMember GSMemInvited IBUnknown
membership <- createContactMember_ db user groupId user invitedMember GCUserMember GSMemInvited (IBContact $ contactId contact)
pure Group {groupId, localDisplayName, groupProfile, members = [member], membership}
-- TODO return the last connection that is ready, not any last connection
-- requires updating connection status
@@ -382,15 +416,15 @@ getGroup_ db User {userId, userContactId} localDisplayName = do
let groupProfile = GroupProfile {displayName, fullName}
in Right (Group {groupId, localDisplayName, groupProfile, members = undefined, membership = undefined}, qInfo)
toGroup _ = Left $ SEGroupNotFound localDisplayName
getMembers_ :: Int64 -> ExceptT StoreError IO [(GroupMember, Maybe Connection)]
getMembers_ :: Int64 -> ExceptT StoreError IO [GroupMember]
getMembers_ 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.display_name, p.full_name,
m.group_member_id, m.member_id, m.member_role, m.member_category, m.member_status,
m.invited_by, m.local_display_name, m.contact_id, p.display_name, p.full_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
@@ -398,19 +432,20 @@ getGroup_ db User {userId, userContactId} localDisplayName = do
LEFT JOIN connections c ON c.connection_id = (
SELECT max(cc.connection_id)
FROM connections cc
where cc.group_member_id = c.group_member_id
where cc.group_member_id = m.group_member_id
)
WHERE m.group_id = ?
WHERE m.group_id = ? AND m.user_id = ?
|]
(Only groupId)
toContactMember :: (GroupMemberRow :. MaybeConnectionRow) -> (GroupMember, Maybe Connection)
toContactMember (memberRow :. connRow) = (toGroupMember userContactId memberRow, toMaybeConnection connRow)
splitUserMember_ :: [(GroupMember, Maybe Connection)] -> Either StoreError ([(GroupMember, Maybe Connection)], GroupMember)
(groupId, userId)
toContactMember :: (GroupMemberRow :. MaybeConnectionRow) -> GroupMember
toContactMember (memberRow :. connRow) =
(toGroupMember userContactId memberRow) {activeConn = toMaybeConnection connRow}
splitUserMember_ :: [GroupMember] -> Either StoreError ([GroupMember], GroupMember)
splitUserMember_ allMembers =
let (b, a) = break ((== Just userContactId) . memberContactId . fst) allMembers
let (b, a) = break ((== Just userContactId) . memberContactId) allMembers
in case a of
[] -> Left SEGroupWithoutUser
u : ms -> Right (b <> ms, fst u)
u : ms -> Right (b <> ms, u)
getGroupInvitation :: StoreMonad m => SQLiteStore -> User -> GroupName -> m ReceivedGroupInvitation
getGroupInvitation st user localDisplayName =
@@ -418,76 +453,364 @@ getGroupInvitation st user localDisplayName =
(Group {membership, members, groupProfile}, qInfo) <- getGroup_ db user localDisplayName
when (memberStatus membership /= GSMemInvited) $ throwError SEGroupAlreadyJoined
case (qInfo, findFromContact (invitedBy membership) members) of
(Just queueInfo, Just (fromMember, Nothing)) ->
pure ReceivedGroupInvitation {fromMember, invitedMember = membership, queueInfo, groupProfile}
_ -> throwError SENoGroupInvitation
(Just queueInfo, Just fromMember) ->
pure ReceivedGroupInvitation {fromMember, userMember = membership, queueInfo, groupProfile}
_ -> throwError SEGroupInvitationNotFound
where
findFromContact :: InvitedBy -> [(GroupMember, Maybe Connection)] -> Maybe (GroupMember, Maybe Connection)
findFromContact (IBContact contactId) = find (\(m, _) -> memberContactId m == Just contactId)
findFromContact :: InvitedBy -> [GroupMember] -> Maybe GroupMember
findFromContact (IBContact contactId) = find ((== Just contactId) . memberContactId)
findFromContact _ = const Nothing
type GroupMemberRow = (Int64, ByteString, GroupMemberRole, GroupMemberStatus, Maybe Int64, Maybe Int64, ContactName, Text)
type GroupMemberRow = (Int64, ByteString, GroupMemberRole, GroupMemberCategory, GroupMemberStatus, Maybe Int64, ContactName, Maybe Int64, ContactName, Text)
toGroupMember :: Int64 -> GroupMemberRow -> GroupMember
toGroupMember userContactId (groupMemberId, memberId, memberRole, memberStatus, invitedById, memberContactId, displayName, fullName) =
toGroupMember userContactId (groupMemberId, memberId, memberRole, memberCategory, memberStatus, invitedById, localDisplayName, memberContactId, displayName, fullName) =
let memberProfile = Profile {displayName, fullName}
invitedBy = toInvitedBy userContactId invitedById
in GroupMember {groupMemberId, memberId, memberRole, memberStatus, invitedBy, memberProfile, memberContactId}
in GroupMember {groupMemberId, memberId, memberRole, memberCategory, memberStatus, invitedBy, localDisplayName, memberProfile, memberContactId, activeConn = Nothing}
createGroupMember :: StoreMonad m => SQLiteStore -> TVar ChaChaDRG -> User -> Int64 -> Contact -> GroupMemberRole -> ConnId -> m GroupMember
createGroupMember st gVar user groupId contact memberRole agentConnId =
createContactGroupMember :: StoreMonad m => SQLiteStore -> TVar ChaChaDRG -> User -> Int64 -> Contact -> GroupMemberRole -> ConnId -> m GroupMember
createContactGroupMember st gVar user groupId contact memberRole agentConnId =
liftIOEither . withTransaction st $ \db ->
createWithRandomId gVar $ \memId -> do
member <- createContactMember_ db user groupId contact (memId, memberRole) GSMemInvited IBUser
member <- createContactMember_ db user groupId contact (memId, memberRole) GCInviteeMember GSMemInvited IBUser
groupMemberId <- insertedRowId db
createMemberConnection_ db (userId user) groupMemberId agentConnId
void $ createMemberConnection_ db (userId user) groupMemberId agentConnId Nothing 0
pure member
createMemberConnection :: MonadUnliftIO m => SQLiteStore -> UserId -> Int64 -> ConnId -> m ()
createMemberConnection st userId groupMemberId agentConnId =
liftIO . withTransaction st $ \db -> createMemberConnection_ db userId groupMemberId agentConnId
liftIO . withTransaction st $ \db ->
void $ createMemberConnection_ db userId groupMemberId agentConnId Nothing 0
updateGroupMemberStatus :: MonadUnliftIO m => SQLiteStore -> UserId -> Int64 -> GroupMemberStatus -> m ()
updateGroupMemberStatus _st _userId _groupMemberId _memberStatus = pure ()
updateGroupMemberStatus st userId groupMemberId memberStatus =
liftIO . withTransaction st $ \db ->
DB.executeNamed
db
[sql|
UPDATE group_members
SET member_status = :member_status
WHERE user_id = :user_id AND group_member_id = :group_member_id
|]
[ ":user_id" := userId,
":group_member_id" := groupMemberId,
":member_status" := memberStatus
]
createMemberConnection_ :: DB.Connection -> UserId -> Int64 -> ConnId -> IO ()
createMemberConnection_ db userId groupMemberId agentConnId =
-- | add new member with profile
createNewGroupMember :: StoreMonad m => SQLiteStore -> User -> Group -> MemberInfo -> GroupMemberCategory -> GroupMemberStatus -> m GroupMember
createNewGroupMember st user@User {userId} group memInfo@(MemberInfo _ _ Profile {displayName, fullName}) memCategory memStatus =
liftIOEither . withTransaction st $ \db ->
withLocalDisplayName db userId displayName $ \localDisplayName -> do
DB.execute db "INSERT INTO contact_profiles (display_name, full_name) VALUES (?, ?)" (displayName, fullName)
memProfileId <- insertedRowId db
let newMember =
NewGroupMember
{ memInfo,
memCategory,
memStatus,
memInvitedBy = IBUnknown,
localDisplayName,
memContactId = Nothing,
memProfileId
}
createNewMember_ db user group newMember
createNewMember_ :: DB.Connection -> User -> Group -> NewGroupMember -> IO GroupMember
createNewMember_
db
User {userId, userContactId}
Group {groupId}
NewGroupMember
{ memInfo = MemberInfo memberId memberRole memberProfile,
memCategory = memberCategory,
memStatus = memberStatus,
memInvitedBy = invitedBy,
localDisplayName,
memContactId = memberContactId,
memProfileId
} = do
let invitedById = fromInvitedBy userContactId invitedBy
DB.execute
db
[sql|
INSERT INTO group_members
(group_id, member_id, member_role, member_category, member_status,
invited_by, user_id, local_display_name, contact_profile_id, contact_id) VALUES (?,?,?,?,?,?,?,?,?,?)
|]
(groupId, memberId, memberRole, memberCategory, memberStatus, invitedById, userId, localDisplayName, memProfileId, memberContactId)
groupMemberId <- insertedRowId db
pure $
GroupMember
{ groupMemberId,
memberId,
memberRole,
memberStatus,
memberCategory,
invitedBy,
memberProfile,
localDisplayName,
memberContactId,
activeConn = Nothing
}
createIntroductions :: MonadUnliftIO m => SQLiteStore -> Group -> GroupMember -> m [GroupMemberIntro]
createIntroductions st Group {members} toMember = do
let reMembers = filter (\m -> memberCurrent m && groupMemberId m /= groupMemberId toMember) members
if null reMembers
then pure []
else liftIO . withTransaction st $ \db ->
mapM (insertIntro_ db) reMembers
where
insertIntro_ :: DB.Connection -> GroupMember -> IO GroupMemberIntro
insertIntro_ db reMember = do
DB.execute
db
[sql|
INSERT INTO group_member_intros
(re_group_member_id, to_group_member_id, intro_status) VALUES (?,?,?)
|]
(groupMemberId reMember, groupMemberId toMember, GMIntroPending)
introId <- insertedRowId db
pure GroupMemberIntro {introId, reMember, toMember, introStatus = GMIntroPending, introInvitation = Nothing}
updateIntroStatus :: MonadUnliftIO m => SQLiteStore -> GroupMemberIntro -> GroupMemberIntroStatus -> m ()
updateIntroStatus st GroupMemberIntro {introId} introStatus' =
liftIO . withTransaction st $ \db ->
DB.executeNamed
db
[sql|
UPDATE group_member_intros
SET intro_status = :intro_status
WHERE group_member_intro_id = :intro_id
|]
[":intro_status" := introStatus', ":intro_id" := introId]
saveIntroInvitation :: StoreMonad m => SQLiteStore -> GroupMember -> GroupMember -> IntroInvitation -> m GroupMemberIntro
saveIntroInvitation st reMember toMember introInv = do
liftIOEither . withTransaction st $ \db -> runExceptT $ do
intro <- getIntroduction_ db reMember toMember
liftIO $
DB.executeNamed
db
[sql|
UPDATE group_member_intros
SET intro_status = :intro_status,
group_queue_info = :group_queue_info,
direct_queue_info = :direct_queue_info
WHERE group_member_intro_id = :intro_id
|]
[ ":intro_status" := GMIntroInvReceived,
":group_queue_info" := groupQInfo introInv,
":direct_queue_info" := directQInfo introInv,
":intro_id" := introId intro
]
pure intro {introInvitation = Just introInv, introStatus = GMIntroInvReceived}
saveMemberInvitation :: StoreMonad m => SQLiteStore -> GroupMember -> IntroInvitation -> m ()
saveMemberInvitation st GroupMember {groupMemberId} IntroInvitation {groupQInfo, directQInfo} =
liftIO . withTransaction st $ \db ->
DB.executeNamed
db
[sql|
UPDATE group_members
SET member_status = :member_status,
group_queue_info = :group_queue_info,
direct_queue_info = :direct_queue_info
WHERE group_member_id = :group_member_id
|]
[ ":member_status" := GSMemIntroInvited,
":group_queue_info" := groupQInfo,
":direct_queue_info" := directQInfo,
":group_member_id" := groupMemberId
]
getIntroduction_ :: DB.Connection -> GroupMember -> GroupMember -> ExceptT StoreError IO GroupMemberIntro
getIntroduction_ db reMember toMember = ExceptT $ do
toIntro
<$> DB.query
db
[sql|
SELECT group_member_intro_id, group_queue_info, direct_queue_info, intro_status
FROM group_member_intros
WHERE re_group_member_id = ? AND to_group_member_id = ?
|]
(groupMemberId reMember, groupMemberId toMember)
where
toIntro :: [(Int64, Maybe SMPQueueInfo, Maybe SMPQueueInfo, GroupMemberIntroStatus)] -> Either StoreError GroupMemberIntro
toIntro [(introId, groupQInfo, directQInfo, introStatus)] =
let introInvitation = IntroInvitation <$> groupQInfo <*> directQInfo
in Right GroupMemberIntro {introId, reMember, toMember, introStatus, introInvitation}
toIntro _ = Left SEIntroNotFound
createIntroReMember :: StoreMonad m => SQLiteStore -> User -> Group -> GroupMember -> MemberInfo -> ConnId -> ConnId -> m GroupMember
createIntroReMember st user@User {userId} group _host@GroupMember {memberContactId, activeConn} memInfo@(MemberInfo _ _ memberProfile) groupAgentConnId directAgentConnId =
liftIOEither . withTransaction st $ \db -> runExceptT $ do
let cLevel = 1 + maybe 0 (connLevel :: Connection -> Int) activeConn
Connection {connId = directConnId} <- liftIO $ createConnection_ db userId directAgentConnId memberContactId cLevel
(localDisplayName, contactId, memProfileId) <- ExceptT $ createContact_ db userId directConnId memberProfile (Just $ groupId group)
liftIO $ do
let newMember =
NewGroupMember
{ memInfo,
memCategory = GCPreMember,
memStatus = GSMemIntroduced,
memInvitedBy = IBUnknown,
localDisplayName,
memContactId = Just contactId,
memProfileId
}
member <- createNewMember_ db user group newMember
conn <- createMemberConnection_ db userId (groupMemberId member) groupAgentConnId memberContactId cLevel
pure (member :: GroupMember) {activeConn = Just conn}
createIntroToMemberContact :: StoreMonad m => SQLiteStore -> UserId -> GroupMember -> GroupMember -> ConnId -> ConnId -> m ()
createIntroToMemberContact st userId GroupMember {memberContactId = viaContactId, activeConn} _to@GroupMember {groupMemberId, localDisplayName} groupAgentConnId directAgentConnId =
liftIO . withTransaction st $ \db -> do
let cLevel = 1 + maybe 0 (connLevel :: Connection -> Int) activeConn
void $ createMemberConnection_ db userId groupMemberId groupAgentConnId viaContactId cLevel
Connection {connId = directConnId} <- createConnection_ db userId directAgentConnId viaContactId cLevel
contactId <- createMemberContact_ db directConnId
updateMember_ db contactId
where
createMemberContact_ :: DB.Connection -> Int64 -> IO Int64
createMemberContact_ db connId = do
DB.executeNamed
db
[sql|
INSERT INTO contacts (contact_profile_id, via_group, local_display_name, user_id)
SELECT contact_profile_id, group_id, :local_display_name, :user_id
FROM group_members
WHERE group_member_id = :group_member_id
|]
[ ":group_member_id" := groupMemberId,
":local_display_name" := localDisplayName,
":user_id" := userId
]
contactId <- insertedRowId db
DB.execute db "UPDATE connections SET contact_id = ? WHERE connection_id = ?" (contactId, connId)
pure contactId
updateMember_ :: DB.Connection -> Int64 -> IO ()
updateMember_ db contactId =
DB.executeNamed
db
[sql|
UPDATE group_members
SET contact_id = :contact_id
WHERE group_member_id = :group_member_id
|]
[":contact_id" := contactId, ":group_member_id" := groupMemberId]
createMemberConnection_ :: DB.Connection -> UserId -> Int64 -> ConnId -> Maybe Int64 -> Int -> IO Connection
createMemberConnection_ db userId groupMemberId agentConnId viaContact connLevel = do
createdAt <- getCurrentTime
DB.execute
db
[sql|
INSERT INTO connections
(user_id, agent_conn_id, conn_status, conn_type, group_member_id) VALUES (?,?,?,?,?);
(user_id, agent_conn_id, conn_status, conn_type, group_member_id, via_contact, conn_level, created_at) VALUES (?,?,?,?,?,?,?,?);
|]
(userId, agentConnId, ConnNew, ConnMember, groupMemberId)
(userId, agentConnId, ConnNew, ConnMember, groupMemberId, viaContact, connLevel, createdAt)
connId <- insertedRowId db
pure Connection {connId, agentConnId, connType = ConnMember, entityId = Just groupMemberId, viaContact, connLevel, connStatus = ConnNew, createdAt}
createContactMember_ :: IsContact a => DB.Connection -> User -> Int64 -> a -> MemberInfo -> GroupMemberStatus -> InvitedBy -> IO GroupMember
createContactMember_ db User {userContactId} groupId userOrContact (memberId, memberRole) memberStatus invitedBy = do
createContactMember_ :: IsContact a => DB.Connection -> User -> Int64 -> a -> (MemberId, GroupMemberRole) -> GroupMemberCategory -> GroupMemberStatus -> InvitedBy -> IO GroupMember
createContactMember_ db User {userId, userContactId} groupId userOrContact (memberId, memberRole) memberCategory memberStatus invitedBy = do
insertMember_
groupMemberId <- insertedRowId db
let memberProfile = profile' userOrContact
memberContactId = Just $ contactId' userOrContact
pure GroupMember {groupMemberId, memberId, memberRole, memberStatus, invitedBy, memberProfile, memberContactId}
localDisplayName = localDisplayName' userOrContact
pure GroupMember {groupMemberId, memberId, memberRole, memberCategory, memberStatus, invitedBy, localDisplayName, memberProfile, memberContactId, activeConn = Nothing}
where
insertMember_ =
DB.executeNamed
db
[sql|
INSERT INTO group_members
( group_id, member_id, member_role, member_status, invited_by,
contact_profile_id, contact_id)
( group_id, member_id, member_role, member_category, member_status, invited_by,
user_id, local_display_name, contact_profile_id, contact_id)
VALUES
(:group_id,:member_id,:member_role,:member_status,:invited_by,
(:group_id,:member_id,:member_role,:member_category,:member_status,:invited_by,
:user_id,:local_display_name,
(SELECT contact_profile_id FROM contacts WHERE contact_id = :contact_id),
:contact_id)
|]
[ ":group_id" := groupId,
":member_id" := memberId,
":member_role" := memberRole,
":member_category" := memberCategory,
":member_status" := memberStatus,
":invited_by" := fromInvitedBy userContactId invitedBy,
":user_id" := userId,
":local_display_name" := localDisplayName' userOrContact,
":contact_id" := contactId' userOrContact
]
getViaGroupMember :: MonadUnliftIO m => SQLiteStore -> User -> Contact -> m (Maybe (GroupName, GroupMember))
getViaGroupMember st User {userId, userContactId} Contact {contactId} =
liftIO . withTransaction st $ \db ->
toGroupAndMember
<$> DB.query
db
[sql|
SELECT
g.local_display_name,
m.group_member_id, m.member_id, m.member_role, m.member_category, m.member_status,
m.invited_by, m.local_display_name, m.contact_id, p.display_name, p.full_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 contacts ct ON ct.contact_id = m.contact_id
JOIN contact_profiles p ON p.contact_profile_id = m.contact_profile_id
JOIN groups g ON g.group_id = m.group_id AND g.group_id = ct.via_group
LEFT JOIN connections c ON c.connection_id = (
SELECT max(cc.connection_id)
FROM connections cc
where cc.group_member_id = m.group_member_id
)
WHERE ct.user_id = ? AND ct.contact_id = ?
|]
(userId, contactId)
where
toGroupAndMember :: [Only GroupName :. GroupMemberRow :. MaybeConnectionRow] -> Maybe (GroupName, GroupMember)
toGroupAndMember [Only groupName :. memberRow :. connRow] =
let member = toGroupMember userContactId memberRow
in Just (groupName, (member :: GroupMember) {activeConn = toMaybeConnection connRow})
toGroupAndMember _ = Nothing
getViaGroupContact :: MonadUnliftIO m => SQLiteStore -> User -> GroupMember -> m (Maybe Contact)
getViaGroupContact st User {userId} GroupMember {groupMemberId} =
liftIO . withTransaction st $ \db ->
toContact
<$> DB.query
db
[sql|
SELECT
ct.contact_id, ct.local_display_name, p.display_name, p.full_name, ct.via_group,
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 contacts ct
JOIN contact_profiles p ON ct.contact_profile_id = p.contact_profile_id
JOIN connections c ON c.connection_id = (
SELECT max(cc.connection_id)
FROM connections cc
where cc.contact_id = ct.contact_id
)
JOIN groups g ON g.group_id = ct.via_group
JOIN group_members m ON m.group_id = g.group_id AND m.contact_id = ct.contact_id
WHERE ct.user_id = ? AND m.group_member_id = ?
|]
(userId, groupMemberId)
where
toContact :: [(Int64, ContactName, Text, Text, Maybe Int64) :. ConnectionRow] -> Maybe Contact
toContact [(contactId, localDisplayName, displayName, fullName, viaGroup) :. connRow] =
let profile = Profile {displayName, fullName}
activeConn = toConnection connRow
in Just Contact {contactId, localDisplayName, profile, activeConn, viaGroup}
toContact _ = Nothing
-- | Saves unique local display name based on passed displayName, suffixed with _N if required.
-- This function should be called inside transaction.
withLocalDisplayName :: forall a. DB.Connection -> UserId -> Text -> (Text -> IO a) -> IO (Either StoreError a)
@@ -548,8 +871,9 @@ data StoreError
| SEGroupWithoutUser
| SEDuplicateGroupMember
| SEGroupAlreadyJoined
| SENoGroupInvitation
| SEGroupInvitationNotFound
| SEConnectionNotFound ConnId
| SEIntroNotFound
| SEUniqueID
| SEInternal ByteString
deriving (Show, Exception)
+1 -1
View File
@@ -15,7 +15,7 @@ import System.Terminal
import System.Terminal.Internal (LocalTerminal, Terminal, VirtualTerminal)
import UnliftIO.STM
data ActiveTo = ActiveNone | ActiveC ContactName
data ActiveTo = ActiveNone | ActiveC ContactName | ActiveG GroupName
deriving (Eq)
data ChatTerminal = ChatTerminal
+180 -20
View File
@@ -26,14 +26,17 @@ import Simplex.Messaging.Agent.Store.SQLite (fromTextField_)
class IsContact a where
contactId' :: a -> Int64
profile' :: a -> Profile
localDisplayName' :: a -> ContactName
instance IsContact User where
contactId' = userContactId
profile' = profile
localDisplayName' = localDisplayName
instance IsContact Contact where
contactId' = contactId
profile' = profile
localDisplayName' = localDisplayName
data User = User
{ userId :: UserId,
@@ -49,7 +52,8 @@ data Contact = Contact
{ contactId :: Int64,
localDisplayName :: ContactName,
profile :: Profile,
activeConn :: Connection
activeConn :: Connection,
viaGroup :: Maybe Int64
}
deriving (Eq, Show)
@@ -64,7 +68,7 @@ data Group = Group
{ groupId :: Int64,
localDisplayName :: GroupName,
groupProfile :: GroupProfile,
members :: [(GroupMember, Maybe Connection)],
members :: [GroupMember],
membership :: GroupMember
}
deriving (Eq, Show)
@@ -90,34 +94,62 @@ instance ToJSON GroupProfile where toEncoding = J.genericToEncoding J.defaultOpt
instance FromJSON GroupProfile
data GroupInvitation = GroupInvitation
{ fromMember :: MemberInfo,
invitedMember :: MemberInfo,
{ fromMember :: (MemberId, GroupMemberRole),
invitedMember :: (MemberId, GroupMemberRole),
queueInfo :: SMPQueueInfo,
groupProfile :: GroupProfile
}
deriving (Eq, Show)
data IntroInvitation = IntroInvitation
{ groupQInfo :: SMPQueueInfo,
directQInfo :: SMPQueueInfo
}
deriving (Eq, Show)
data MemberInfo = MemberInfo MemberId GroupMemberRole Profile
deriving (Eq, Show)
memberInfo :: GroupMember -> MemberInfo
memberInfo m = MemberInfo (memberId m) (memberRole m) (memberProfile m)
data ReceivedGroupInvitation = ReceivedGroupInvitation
{ fromMember :: GroupMember,
invitedMember :: GroupMember,
userMember :: GroupMember,
queueInfo :: SMPQueueInfo,
groupProfile :: GroupProfile
}
deriving (Eq, Show)
type MemberInfo = (MemberId, GroupMemberRole)
data GroupMember = GroupMember
{ groupMemberId :: Int64,
memberId :: MemberId,
memberRole :: GroupMemberRole,
memberCategory :: GroupMemberCategory,
memberStatus :: GroupMemberStatus,
invitedBy :: InvitedBy,
localDisplayName :: ContactName,
memberProfile :: Profile,
memberContactId :: Maybe Int64
memberContactId :: Maybe Int64,
activeConn :: Maybe Connection
}
deriving (Eq, Show)
memberConnId :: GroupMember -> Maybe ConnId
memberConnId GroupMember {activeConn} = case activeConn of
Just Connection {agentConnId} -> Just agentConnId
Nothing -> Nothing
data NewGroupMember = NewGroupMember
{ memInfo :: MemberInfo,
memCategory :: GroupMemberCategory,
memStatus :: GroupMemberStatus,
memInvitedBy :: InvitedBy,
localDisplayName :: ContactName,
memProfileId :: Int64,
memContactId :: Maybe Int64
}
type MemberId = ByteString
data InvitedBy = IBContact Int64 | IBUser | IBUnknown
@@ -160,37 +192,107 @@ 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)
Left e -> returnError ConversionFailed f ("could not parse field: " ++ e)
f -> returnError ConversionFailed f "expecting SQLBlob column type"
data GroupMemberCategory
= GCUserMember
| GCInviteeMember -- member invited by the user
| GCHostMember -- member who invited the user
| GCPreMember -- member who joined before the user and was introduced to the user (user receives x.grp.mem.intro about such members)
| GCPostMember -- member who joined after the user to whom the user was introduced (user receives x.grp.mem.new announcing these members and then x.grp.mem.fwd with invitation from these members)
deriving (Eq, Show)
instance FromField GroupMemberCategory where fromField = fromTextField_ memberCategoryT
instance ToField GroupMemberCategory where toField = toField . serializeMemberCategory
memberCategoryT :: Text -> Maybe GroupMemberCategory
memberCategoryT = \case
"user" -> Just GCUserMember
"invitee" -> Just GCInviteeMember
"host" -> Just GCHostMember
"pre" -> Just GCPreMember
"post" -> Just GCPostMember
_ -> Nothing
serializeMemberCategory :: GroupMemberCategory -> Text
serializeMemberCategory = \case
GCUserMember -> "user"
GCInviteeMember -> "invitee"
GCHostMember -> "host"
GCPreMember -> "pre"
GCPostMember -> "post"
data GroupMemberStatus
= GSMemInvited -- member received (or sent to) invitation
| GSMemAccepted -- member accepted invitation
= GSMemRemoved -- member who was removed from the group
| GSMemLeft -- member who left the group
| GSMemInvited -- member is sent to or received invitation to join the group
| GSMemIntroduced -- user received x.grp.mem.intro for this member (only with GCPreMember)
| GSMemIntroInvited -- member is sent to or received from intro invitation
| GSMemAccepted -- member accepted invitation (only User and Invitee)
| GSMemAnnounced -- host announced (x.grp.mem.new) a member (Invitee and PostMember) to the group - at this point this member can send messages and invite other members (if they have sufficient permissions)
| GSMemConnected -- member created the group connection with the inviting member
| GSMemReady -- member connections are forwarded to all previous members
| GSMemFull -- member created group connections with all previous members
| GSMemComplete -- host confirmed (x.grp.mem.all) that a member (User, Invitee and PostMember) created group connections with all previous members
| GSMemCreator -- user member that created the group (only GCUserMember)
deriving (Eq, Show, Ord)
instance FromField GroupMemberStatus where fromField = fromTextField_ memberStatusT
instance ToField GroupMemberStatus where toField = toField . serializeMemberStatus
memberActive :: GroupMember -> Bool
memberActive m = case memberStatus m of
GSMemRemoved -> False
GSMemLeft -> False
GSMemInvited -> False
GSMemIntroduced -> False
GSMemIntroInvited -> False
GSMemAccepted -> False
GSMemAnnounced -> False
GSMemConnected -> True
GSMemComplete -> True
GSMemCreator -> True
memberCurrent :: GroupMember -> Bool
memberCurrent m = case memberStatus m of
GSMemRemoved -> False
GSMemLeft -> False
GSMemInvited -> False
GSMemIntroduced -> True
GSMemIntroInvited -> True
GSMemAccepted -> True
GSMemAnnounced -> True
GSMemConnected -> True
GSMemComplete -> True
GSMemCreator -> True
memberStatusT :: Text -> Maybe GroupMemberStatus
memberStatusT = \case
"removed" -> Just GSMemRemoved
"left" -> Just GSMemLeft
"invited" -> Just GSMemInvited
"introduced" -> Just GSMemIntroduced
"intro-inv" -> Just GSMemIntroInvited
"accepted" -> Just GSMemAccepted
"announced" -> Just GSMemAnnounced
"connected" -> Just GSMemConnected
"ready" -> Just GSMemReady
"full" -> Just GSMemFull
"complete" -> Just GSMemComplete
"creator" -> Just GSMemCreator
_ -> Nothing
serializeMemberStatus :: GroupMemberStatus -> Text
serializeMemberStatus = \case
GSMemRemoved -> "removed"
GSMemLeft -> "left"
GSMemInvited -> "invited"
GSMemIntroduced -> "introduced"
GSMemIntroInvited -> "intro-inv"
GSMemAccepted -> "accepted"
GSMemAnnounced -> "announced"
GSMemConnected -> "connected"
GSMemReady -> "ready"
GSMemFull -> "full"
GSMemComplete -> "complete"
GSMemCreator -> "creator"
data Connection = Connection
{ connId :: Int64,
@@ -204,7 +306,19 @@ data Connection = Connection
}
deriving (Eq, Show)
data ConnStatus = ConnNew | ConnConfirmed | ConnAccepted | ConnReady
data ConnStatus
= -- | connection is created by initiating party with agent NEW command (createConnection)
ConnNew
| -- | connection is joined by joining party with agent JOIN command (joinConnection)
ConnJoined
| -- | initiating party received CONF notification (to be renamed to REQ)
ConnRequested
| -- | initiating party accepted connection with agent LET command (to be renamed to ACPT) (allowConnection)
ConnAccepted
| -- | connection can be sent messages to (after joining party received INFO notification)
ConnSndReady
| -- | connection is ready for both parties to send and receive messages
ConnReady
deriving (Eq, Show)
instance FromField ConnStatus where fromField = fromTextField_ connStatusT
@@ -214,16 +328,20 @@ instance ToField ConnStatus where toField = toField . serializeConnStatus
connStatusT :: Text -> Maybe ConnStatus
connStatusT = \case
"new" -> Just ConnNew
"confirmed" -> Just ConnConfirmed
"joined" -> Just ConnJoined
"requested" -> Just ConnRequested
"accepted" -> Just ConnAccepted
"snd-ready" -> Just ConnSndReady
"ready" -> Just ConnReady
_ -> Nothing
serializeConnStatus :: ConnStatus -> Text
serializeConnStatus = \case
ConnNew -> "new"
ConnConfirmed -> "confirmed"
ConnJoined -> "joined"
ConnRequested -> "requested"
ConnAccepted -> "accepted"
ConnSndReady -> "snd-ready"
ConnReady -> "ready"
data ConnType = ConnContact | ConnMember
@@ -249,3 +367,45 @@ data NewConnection = NewConnection
connLevel :: Int,
viaConn :: Maybe Int64
}
data GroupMemberIntro = GroupMemberIntro
{ introId :: Int64,
reMember :: GroupMember,
toMember :: GroupMember,
introStatus :: GroupMemberIntroStatus,
introInvitation :: Maybe IntroInvitation
}
data GroupMemberIntroStatus
= GMIntroPending
| GMIntroSent
| GMIntroInvReceived
| GMIntroInvForwarded
| GMIntroReConnected
| GMIntroToConnected
| GMIntroConnected
instance FromField GroupMemberIntroStatus where fromField = fromTextField_ introStatusT
instance ToField GroupMemberIntroStatus where toField = toField . serializeIntroStatus
introStatusT :: Text -> Maybe GroupMemberIntroStatus
introStatusT = \case
"new" -> Just GMIntroPending
"sent" -> Just GMIntroSent
"rcv" -> Just GMIntroInvReceived
"fwd" -> Just GMIntroInvForwarded
"re-con" -> Just GMIntroReConnected
"to-con" -> Just GMIntroToConnected
"con" -> Just GMIntroConnected
_ -> Nothing
serializeIntroStatus :: GroupMemberIntroStatus -> Text
serializeIntroStatus = \case
GMIntroPending -> "new"
GMIntroSent -> "sent"
GMIntroInvReceived -> "rcv"
GMIntroInvForwarded -> "fwd"
GMIntroReConnected -> "re-con"
GMIntroToConnected -> "to-con"
GMIntroConnected -> "con"
+82 -34
View File
@@ -13,12 +13,16 @@ module Simplex.Chat.View
showContactConnected,
showContactDisconnected,
showReceivedMessage,
showReceivedGroupMessage,
showSentMessage,
showSentGroupMessage,
showGroupCreated,
showSentGroupInvitation,
showReceivedGroupInvitation,
showConnectedGroupMember,
showUserConnectedToGroup,
showJoinedGroupMember,
showUserJoinedGroup,
showJoinedGroupMemberConnecting,
showConnectedToGroupMember,
safeDecodeUtf8,
)
where
@@ -26,7 +30,7 @@ where
import Control.Monad.IO.Unlift
import Control.Monad.Reader
import Data.ByteString.Char8 (ByteString)
import Data.Composition ((.:))
import Data.Composition ((.:), (.:.))
import Data.Text (Text)
import qualified Data.Text as T
import Data.Time.Clock (DiffTime, UTCTime)
@@ -60,10 +64,22 @@ showContactDisconnected :: ChatReader m => ContactName -> m ()
showContactDisconnected = printToView . contactDisconnected
showReceivedMessage :: ChatReader m => ContactName -> UTCTime -> Text -> MsgIntegrity -> m ()
showReceivedMessage c utcTime msg mOk = printToView =<< liftIO (receivedMessage c utcTime msg mOk)
showReceivedMessage = showReceivedMessage_ . ttyFromContact
showReceivedGroupMessage :: ChatReader m => GroupName -> ContactName -> UTCTime -> Text -> MsgIntegrity -> m ()
showReceivedGroupMessage = showReceivedMessage_ .: ttyFromGroup
showReceivedMessage_ :: ChatReader m => StyledString -> UTCTime -> Text -> MsgIntegrity -> m ()
showReceivedMessage_ from utcTime msg mOk = printToView =<< liftIO (receivedMessage from utcTime msg mOk)
showSentMessage :: ChatReader m => ContactName -> ByteString -> m ()
showSentMessage c msg = printToView =<< liftIO (sentMessage c msg)
showSentMessage = showSentMessage_ . ttyToContact
showSentGroupMessage :: ChatReader m => GroupName -> ByteString -> m ()
showSentGroupMessage = showSentMessage_ . ttyToGroup
showSentMessage_ :: ChatReader m => StyledString -> ByteString -> m ()
showSentMessage_ to msg = printToView =<< liftIO (sentMessage to msg)
showGroupCreated :: ChatReader m => Group -> m ()
showGroupCreated = printToView . groupCreated
@@ -71,14 +87,20 @@ showGroupCreated = printToView . groupCreated
showSentGroupInvitation :: ChatReader m => Group -> ContactName -> m ()
showSentGroupInvitation = printToView .: sentGroupInvitation
showReceivedGroupInvitation :: ChatReader m => Group -> ContactName -> m ()
showReceivedGroupInvitation = printToView .: receivedGroupInvitation
showReceivedGroupInvitation :: ChatReader m => Group -> ContactName -> GroupMemberRole -> m ()
showReceivedGroupInvitation = printToView .:. receivedGroupInvitation
showConnectedGroupMember :: ChatReader m => GroupName -> ContactName -> m ()
showConnectedGroupMember = printToView .: connectedGroupMember
showJoinedGroupMember :: ChatReader m => GroupName -> GroupMember -> m ()
showJoinedGroupMember = printToView .: joinedGroupMember
showUserConnectedToGroup :: ChatReader m => GroupName -> m ()
showUserConnectedToGroup = printToView . userConnectedToGroup
showUserJoinedGroup :: ChatReader m => GroupName -> m ()
showUserJoinedGroup = printToView . userJoinedGroup
showJoinedGroupMemberConnecting :: ChatReader m => GroupName -> GroupMember -> GroupMember -> m ()
showJoinedGroupMemberConnecting = printToView .:. joinedGroupMemberConnecting
showConnectedToGroupMember :: ChatReader m => GroupName -> GroupMember -> m ()
showConnectedToGroupMember = printToView .: connectedToGroupMember
invitation :: SMPQueueInfo -> [StyledString]
invitation qInfo =
@@ -101,28 +123,40 @@ contactDisconnected c = ["disconnected from " <> ttyContact c <> " - restart cha
groupCreated :: Group -> [StyledString]
groupCreated g@Group {localDisplayName} =
[ "group " <> ttyFullGroup g <> " is created",
"use " <> highlight ("/a #" <> localDisplayName <> " <name>") <> " to add members"
"use " <> highlight ("/a " <> localDisplayName <> " <name>") <> " to add members"
]
sentGroupInvitation :: Group -> ContactName -> [StyledString]
sentGroupInvitation g c = ["invitation to join the group " <> ttyFullGroup g <> " sent to " <> ttyContact c]
receivedGroupInvitation :: Group -> ContactName -> [StyledString]
receivedGroupInvitation g@Group {localDisplayName} c =
[ ttyContact c <> " invites you to join the group " <> ttyFullGroup g,
"use " <> highlight ("/j #" <> localDisplayName) <> " to accept"
receivedGroupInvitation :: Group -> ContactName -> GroupMemberRole -> [StyledString]
receivedGroupInvitation g@Group {localDisplayName} c role =
[ ttyFullGroup g <> ": " <> ttyContact c <> " invites you to join the group as " <> plain (serializeMemberRole role),
"use " <> highlight ("/j " <> localDisplayName) <> " to accept"
]
connectedGroupMember :: GroupName -> ContactName -> [StyledString]
connectedGroupMember g c = [ttyContact c <> " joined the group " <> ttyGroup g]
joinedGroupMember :: GroupName -> GroupMember -> [StyledString]
joinedGroupMember g m = [ttyGroup g <> ": " <> ttyMember m <> " joined the group "]
userConnectedToGroup :: GroupName -> [StyledString]
userConnectedToGroup g = ["you joined the group " <> ttyGroup g]
userJoinedGroup :: GroupName -> [StyledString]
userJoinedGroup g = [ttyGroup g <> ": you joined the group"]
receivedMessage :: ContactName -> UTCTime -> Text -> MsgIntegrity -> IO [StyledString]
receivedMessage c utcTime msg mOk = do
joinedGroupMemberConnecting :: GroupName -> GroupMember -> GroupMember -> [StyledString]
joinedGroupMemberConnecting g host m = [ttyGroup g <> ": " <> ttyMember host <> " added " <> ttyFullMember m <> " to the group (connecting...)"]
connectedToGroupMember :: GroupName -> GroupMember -> [StyledString]
connectedToGroupMember g m = [ttyGroup g <> ": " <> connectedMember m <> " is connected"]
connectedMember :: GroupMember -> StyledString
connectedMember m = case memberCategory m of
GCPreMember -> "member " <> ttyFullMember m
GCPostMember -> "new member " <> ttyMember m -- without fullName as as it was shown in joinedGroupMemberConnecting
_ -> "member " <> ttyMember m -- these case is not used
receivedMessage :: StyledString -> UTCTime -> Text -> MsgIntegrity -> IO [StyledString]
receivedMessage from utcTime msg mOk = do
t <- formatUTCTime <$> getCurrentTimeZone <*> getZonedTime
pure $ prependFirst (t <> " " <> ttyFromContact c) (msgPlain msg) ++ showIntegrity mOk
pure $ prependFirst (t <> " " <> from) (msgPlain msg) ++ showIntegrity mOk
where
formatUTCTime :: TimeZone -> ZonedTime -> StyledString
formatUTCTime localTz currentTime =
@@ -145,10 +179,10 @@ receivedMessage c utcTime msg mOk = do
msgError :: String -> [StyledString]
msgError s = [styled (Colored Red) s]
sentMessage :: ContactName -> ByteString -> IO [StyledString]
sentMessage c msg = do
sentMessage :: StyledString -> ByteString -> IO [StyledString]
sentMessage to msg = do
time <- formatTime defaultTimeLocale "%H:%M" <$> getZonedTime
pure $ prependFirst (styleTime time <> " " <> ttyToContact c) (msgPlain $ safeDecodeUtf8 msg)
pure $ prependFirst (styleTime time <> " " <> to) (msgPlain $ safeDecodeUtf8 msg)
prependFirst :: StyledString -> [StyledString] -> [StyledString]
prependFirst s [] = [s]
@@ -162,9 +196,10 @@ chatError = \case
ChatError err -> case err of
CEGroupDuplicateMember c -> ["contact " <> ttyContact c <> " is already in the group"]
CEGroupDuplicateMemberId -> ["cannot add member - duplicate member ID"]
CEGroupRole -> ["insufficient role for this group command"]
CEGroupUserRole -> ["you have insufficient permissions for this group command"]
CEGroupContactRole c -> ["contact " <> ttyContact c <> " has insufficient permissions for this group action"]
CEGroupNotJoined g -> ["you did not join this group, use " <> highlight ("/join #" <> g)]
CEGroupMemberNotReady -> ["you cannot invite other members yet, try later"]
CEGroupMemberNotActive -> ["you cannot invite other members yet, try later"]
CEGroupInternal s -> ["chat group bug: " <> plain s]
-- e -> ["chat error: " <> plain (show e)]
ChatErrorStore err -> case err of
@@ -191,7 +226,17 @@ ttyContact = styled (Colored Green)
ttyFullContact :: Contact -> StyledString
ttyFullContact Contact {localDisplayName, profile = Profile {fullName}} =
ttyContact localDisplayName <> optFullName localDisplayName fullName
ttyFullName localDisplayName fullName
ttyMember :: GroupMember -> StyledString
ttyMember GroupMember {localDisplayName} = ttyContact localDisplayName
ttyFullMember :: GroupMember -> StyledString
ttyFullMember GroupMember {localDisplayName, memberProfile = Profile {fullName}} =
ttyFullName localDisplayName fullName
ttyFullName :: ContactName -> Text -> StyledString
ttyFullName c fullName = ttyContact c <> optFullName c fullName
ttyToContact :: ContactName -> StyledString
ttyToContact c = styled (Colored Cyan) $ "@" <> c <> " "
@@ -206,9 +251,15 @@ ttyFullGroup :: Group -> StyledString
ttyFullGroup Group {localDisplayName, groupProfile = GroupProfile {fullName}} =
ttyGroup localDisplayName <> optFullName localDisplayName fullName
optFullName :: Text -> Text -> StyledString
ttyFromGroup :: GroupName -> ContactName -> StyledString
ttyFromGroup g c = styled (Colored Yellow) $ "#" <> g <> " " <> c <> "> "
ttyToGroup :: GroupName -> StyledString
ttyToGroup g = styled (Colored Cyan) $ "#" <> g <> " "
optFullName :: ContactName -> Text -> StyledString
optFullName localDisplayName fullName
| localDisplayName == fullName = ""
| T.null fullName || localDisplayName == fullName = ""
| otherwise = plain (" (" <> fullName <> ")")
highlight :: StyledFormat a => a -> StyledString
@@ -217,8 +268,5 @@ highlight = styled (Colored Cyan)
highlight' :: String -> StyledString
highlight' = highlight
-- ttyFromGroup :: Group -> Contact -> StyledString
-- ttyFromGroup (Group g) (Contact a) = styled (Colored Yellow) $ "#" <> g <> " " <> a <> "> "
styleTime :: String -> StyledString
styleTime = Styled [SetColor Foreground Vivid Black]
+11 -5
View File
@@ -75,7 +75,12 @@ A -> B: invite to group - `MSG: x.grp.inv G_MEM_ID_A,G_MEM_ROLE_A,G_MEM_ID_B,G_M
user B confirms
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>`
A -> group (including B)): announce group member: `MSG: N x.grp.mem.new G_MEM_ID_B,G_MEM_ROLE_B,G_MEM_ID_M,... x.json:NNN <B_profile>`
In the message `x.grp.mem.new` A sends the sorted list of all members to whom A is connected followed by the new member ID, role and profile. The following introductions will be sent about/to all members A "knows about" (includes members introduced to A and members who accepted group invitation but not connected yet), once they are connected, so it can be a bigger list than sent in `x.grp.mem.new`.
All members who received `x.grp.mem.new` from A should check the list of connected members and if any connected members that recipients invited to the group are not in this list, they should introduce them to this new member (the last ID, role and profile in `x.grp.mem.new`). That might lead to double introductions that would provide a stronger consistency of group membership at a cost of extra connection between some members that will be unused.
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_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>`
@@ -83,16 +88,17 @@ 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>`
M -> Bg: establish group connection (M: JOIN, B: LET)
M -> B: establish direct connection (M: JOIN, B: LET)
M -> Bg: confirm profile and role - `MSG: x.grp.mem.info G_MEM_ID_M,G_MEM_ROLE x.json:NNN <M_profile>`
M -> Bg: confirm profile and role - `CONF: x.grp.mem.info G_MEM_ID_M,G_MEM_ROLE x.json:NNN <M_profile>`
B -> Mg: send profile probe - `MSG: x.info.probe <probe>` - it should always be send, even when there is no profile match.
if M is a known contact (profile match) send probe to M:
B -> M (via old DM conn): profile match probe: `MSG: x.grp.mem.probe G_MEM_ID_B,<probe_hash>`
M -> B (via old DM conn): probe confirm: `MSG: x.grp.mem.probe.ok G_MEM_ID_M,<probe>`
B -> M (via old DM conn): profile match probe: `MSG: x.info.probe.check G_MEM_ID_B,<probe_hash>`
M -> B (via old DM conn): probe confirm: `MSG: x.info.probe.ok G_MEM_ID_M,<probe>`
link to the same contact
B -> Ag: connected to M: `MSG: x.grp.mem.con G_MEM_ID_M`
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`
A -> group: `MSG: N x.grp.mem.con.all G_MEM_ID_B`
#### Send group message