mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
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:
committed by
GitHub
parent
94f89ed8f7
commit
189cd7e09d
+248
-89
@@ -23,7 +23,7 @@ import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Functor (($>))
|
||||
import Data.List (find)
|
||||
import Data.Maybe (isJust, isNothing)
|
||||
import Data.Maybe (isJust)
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import Data.Text.Encoding (encodeUtf8)
|
||||
@@ -77,7 +77,7 @@ cfg =
|
||||
connIdBytes = 12,
|
||||
tbqSize = 16,
|
||||
dbFile = undefined, -- filled in from options
|
||||
dbPoolSize = 4,
|
||||
dbPoolSize = 1,
|
||||
smpCfg = smpDefaultConfig
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ logCfg :: LogConfig
|
||||
logCfg = LogConfig {lc_file = Nothing, lc_stderr = True}
|
||||
|
||||
simplexChat :: WithTerminal t => ChatOpts -> t -> IO ()
|
||||
simplexChat opts t = do
|
||||
simplexChat opts t =
|
||||
-- setLogLevel LogInfo -- LogError
|
||||
-- withGlobalLogging logCfg $ do
|
||||
initializeNotifications
|
||||
@@ -94,7 +94,7 @@ simplexChat opts t = do
|
||||
|
||||
newChatController :: WithTerminal t => ChatOpts -> t -> (Notification -> IO ()) -> IO ChatController
|
||||
newChatController ChatOpts {dbFile, smpServers} t sendNotification = do
|
||||
chatStore <- createStore (dbFile <> ".chat.db") 4
|
||||
chatStore <- createStore (dbFile <> ".chat.db") 1
|
||||
currentUser <- getCreateActiveUser chatStore
|
||||
chatTerminal <- newChatTerminal t
|
||||
smpAgent <- getSMPAgentClient cfg {dbFile = dbFile <> ".agent.db", smpServers}
|
||||
@@ -126,6 +126,7 @@ inputSubscriber = do
|
||||
Right cmd -> do
|
||||
case cmd of
|
||||
SendMessage c msg -> showSentMessage c msg
|
||||
SendGroupMessage g msg -> showSentGroupMessage g msg
|
||||
_ -> printToView [plain s]
|
||||
user <- asks currentUser
|
||||
void . runExceptT $ processChatCommand user cmd `catchError` showChatError
|
||||
@@ -150,7 +151,7 @@ processChatCommand user@User {userId, profile} = \case
|
||||
showContactDeleted cName
|
||||
SendMessage cName msg -> do
|
||||
contact <- withStore $ \st -> getContact st userId cName
|
||||
let msgEvent = XMsgNew MTText [] [MsgBodyContent {contentType = SimplexContentType XCText, contentData = msg}]
|
||||
let msgEvent = XMsgNew $ MsgContent MTText [] [MsgContentBody {contentType = SimplexContentType XCText, contentData = msg}]
|
||||
sendDirectMessage (contactConnId contact) msgEvent
|
||||
setActive $ ActiveC cName
|
||||
NewGroup gProfile -> do
|
||||
@@ -162,29 +163,39 @@ processChatCommand user@User {userId, profile} = \case
|
||||
let Group {groupId, groupProfile, membership, members} = group
|
||||
userRole = memberRole membership
|
||||
userMemberId = memberId membership
|
||||
when (userRole < GRAdmin || userRole < memRole) $ throwError $ ChatError CEGroupRole
|
||||
when (userRole < GRAdmin || userRole < memRole) $ throwError $ ChatError CEGroupUserRole
|
||||
when (isMember contact members) . throwError . ChatError $ CEGroupDuplicateMember cName
|
||||
when (memberStatus membership == GSMemInvited) . throwError . ChatError $ CEGroupNotJoined gName
|
||||
when (memberStatus membership < GSMemReady) . throwError . ChatError $ CEGroupMemberNotReady
|
||||
unless (memberActive membership) . throwError . ChatError $ CEGroupMemberNotActive
|
||||
gVar <- asks idsDrg
|
||||
(agentConnId, qInfo) <- withAgent createConnection
|
||||
GroupMember {memberId} <- withStore $ \st -> createGroupMember st gVar user groupId contact memRole agentConnId
|
||||
GroupMember {memberId} <- withStore $ \st -> createContactGroupMember st gVar user groupId contact memRole agentConnId
|
||||
let msg = XGrpInv $ GroupInvitation (userMemberId, userRole) (memberId, memRole) qInfo groupProfile
|
||||
sendDirectMessage (contactConnId contact) msg
|
||||
showSentGroupInvitation group cName
|
||||
setActive $ ActiveG gName
|
||||
JoinGroup gName -> do
|
||||
ReceivedGroupInvitation {fromMember, invitedMember, queueInfo} <- withStore $ \st -> getGroupInvitation st user gName
|
||||
agentConnId <- withAgent $ \a -> joinConnection a queueInfo . directMessage . XGrpAcpt $ memberId invitedMember
|
||||
withStore $ \st -> createMemberConnection st userId (groupMemberId fromMember) agentConnId
|
||||
MemberRole _gRef _cRef _mRole -> pure ()
|
||||
RemoveMember _gRef _cRef -> pure ()
|
||||
LeaveGroup _gRef -> pure ()
|
||||
DeleteGroup _gRef -> pure ()
|
||||
ListMembers _gRef -> pure ()
|
||||
SendGroupMessage _gRef _msg -> pure ()
|
||||
ReceivedGroupInvitation {fromMember, userMember, queueInfo} <- withStore $ \st -> getGroupInvitation st user gName
|
||||
agentConnId <- withAgent $ \a -> joinConnection a queueInfo . directMessage . XGrpAcpt $ memberId userMember
|
||||
withStore $ \st -> do
|
||||
createMemberConnection st userId (groupMemberId fromMember) agentConnId
|
||||
updateGroupMemberStatus st userId (groupMemberId fromMember) GSMemAccepted
|
||||
updateGroupMemberStatus st userId (groupMemberId userMember) GSMemAccepted
|
||||
MemberRole _gName _cName _mRole -> pure ()
|
||||
RemoveMember _gName _cName -> pure ()
|
||||
LeaveGroup _gName -> pure ()
|
||||
DeleteGroup _gName -> pure ()
|
||||
ListMembers _gName -> pure ()
|
||||
SendGroupMessage gName msg -> do
|
||||
-- TODO save sent messages
|
||||
-- TODO save pending message delivery for members without connections
|
||||
Group {members} <- withStore $ \st -> getGroup st user gName
|
||||
let msgEvent = XMsgNew $ MsgContent MTText [] [MsgContentBody {contentType = SimplexContentType XCText, contentData = msg}]
|
||||
sendGroupMessage members msgEvent
|
||||
setActive $ ActiveG gName
|
||||
where
|
||||
isMember :: Contact -> [(GroupMember, Maybe Connection)] -> Bool
|
||||
isMember Contact {contactId} members = isJust $ find ((== Just contactId) . memberContactId . fst) members
|
||||
isMember :: Contact -> [GroupMember] -> Bool
|
||||
isMember Contact {contactId} members = isJust $ find ((== Just contactId) . memberContactId) members
|
||||
|
||||
agentSubscriber :: (MonadUnliftIO m, MonadReader ChatController m) => m ()
|
||||
agentSubscriber = do
|
||||
@@ -199,92 +210,230 @@ processAgentMessage :: forall m. ChatMonad m => User -> ConnId -> ACommand 'Agen
|
||||
processAgentMessage user@User {userId, profile} agentConnId agentMessage = do
|
||||
chatDirection <- withStore $ \st -> getConnectionChatDirection st user agentConnId
|
||||
case chatDirection of
|
||||
ReceivedDirectMessage (CContact ct@Contact {localDisplayName = c}) ->
|
||||
ReceivedDMContact ct@Contact {localDisplayName = c, activeConn} ->
|
||||
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
|
||||
XMsgNew (MsgContent MTText [] body) -> newTextMessage c meta $ find (isSimplexContentType XCText) body
|
||||
XInfo _ -> pure () -- TODO profile update
|
||||
XGrpInv gInv -> saveGroupInvitation ct gInv
|
||||
XGrpInv gInv -> processGroupInvitation ct gInv
|
||||
_ -> pure ()
|
||||
CON -> do
|
||||
-- TODO update connection status
|
||||
showContactConnected ct
|
||||
showToast ("@" <> c) "connected"
|
||||
setActive $ ActiveC c
|
||||
END -> do
|
||||
showContactDisconnected c
|
||||
showToast ("@" <> c) "disconnected"
|
||||
unsetActive $ ActiveC c
|
||||
_ -> pure ()
|
||||
ReceivedDirectMessage (CConnection conn) ->
|
||||
case agentMessage of
|
||||
CONF confId connInfo -> do
|
||||
-- TODO update connection status
|
||||
saveConnInfo conn connInfo
|
||||
withAgent $ \a -> allowConnection a agentConnId confId . directMessage $ XInfo profile
|
||||
INFO connInfo ->
|
||||
saveConnInfo conn connInfo
|
||||
_ -> pure ()
|
||||
ReceivedGroupMessage gName m ->
|
||||
case agentMessage of
|
||||
CONF confId connInfo -> do
|
||||
-- confirming direct connection with a member
|
||||
withStore $ \st -> updateConnectionStatus st activeConn ConnRequested
|
||||
ChatMessage {chatMsgEvent} <- liftEither $ parseChatMessage connInfo
|
||||
case chatMsgEvent of
|
||||
XGrpAcpt memId
|
||||
| memId == memberId m -> do
|
||||
withStore $ \st -> updateGroupMemberStatus st userId (groupMemberId m) GSMemAccepted
|
||||
withAgent $ \a -> allowConnection a agentConnId confId ""
|
||||
| otherwise -> pure () -- TODO error not matching member ID
|
||||
_ -> pure () -- TODO show/log error, other events in SMP confirmation
|
||||
XGrpMemInfo _memId _memProfile -> do
|
||||
-- TODO check member ID
|
||||
-- TODO update member profile
|
||||
acceptAgentConnection activeConn confId XOk
|
||||
_ -> messageError "CONF from member must have x.grp.mem.info"
|
||||
INFO connInfo -> do
|
||||
withStore $ \st -> updateConnectionStatus st activeConn ConnSndReady
|
||||
ChatMessage {chatMsgEvent} <- liftEither $ parseChatMessage connInfo
|
||||
case chatMsgEvent of
|
||||
XGrpMemInfo _memId _memProfile -> do
|
||||
-- TODO check member ID
|
||||
-- TODO update member profile
|
||||
pure ()
|
||||
XOk -> pure ()
|
||||
_ -> messageError "INFO from member must have x.grp.mem.info or x.ok"
|
||||
CON -> do
|
||||
Group {membership, members} <- withStore $ \st -> getGroup st user gName
|
||||
-- TODO because the contact is not created instantly, if the member is not created from contact,
|
||||
-- it should still have a unique local display name.
|
||||
-- If it is created from contact it can still be duplicated on the member (and match the contact)
|
||||
case invitedBy m of
|
||||
IBUser -> do
|
||||
-- sender was invited by the current user
|
||||
withStore $ \st -> updateGroupMemberStatus st userId (groupMemberId m) GSMemConnected
|
||||
sendGroupMessage members $ XGrpMemNew (memberId m) (memberRole m) (memberProfile m)
|
||||
showConnectedGroupMember gName $ displayName (memberProfile m :: Profile)
|
||||
forM_ (filter (\m' -> memberStatus m' >= GSMemConnected) . map fst $ members) $ \m' ->
|
||||
sendDirectMessage agentConnId $ XGrpMemIntro (memberId m') (memberRole m') (memberProfile m')
|
||||
withStore $ \st -> updateConnectionStatus st activeConn ConnReady
|
||||
withStore (\st -> getViaGroupMember st user ct) >>= \case
|
||||
Nothing -> do
|
||||
showContactConnected ct
|
||||
setActive $ ActiveC c
|
||||
showToast (c <> "> ") "connected"
|
||||
Just (gName, m) ->
|
||||
when (memberIsReady m) $ notifyMemberConnected gName m
|
||||
END -> do
|
||||
showContactDisconnected c
|
||||
showToast (c <> "> ") "disconnected"
|
||||
unsetActive $ ActiveC c
|
||||
_ -> messageError $ "unexpected agent event: " <> T.pack (show agentMessage)
|
||||
ReceivedDMConnection conn ->
|
||||
case agentMessage of
|
||||
CONF confId connInfo -> do
|
||||
withStore $ \st -> updateConnectionStatus st conn ConnRequested
|
||||
saveConnInfo conn connInfo
|
||||
acceptAgentConnection conn confId $ XInfo profile
|
||||
INFO connInfo -> do
|
||||
withStore $ \st -> updateConnectionStatus st conn ConnSndReady
|
||||
saveConnInfo conn connInfo
|
||||
CON ->
|
||||
withStore $ \st -> updateConnectionStatus st conn ConnReady
|
||||
_ -> messageError $ "unsupported agent event: " <> T.pack (show agentMessage)
|
||||
ReceivedGroupMessage conn gName m ->
|
||||
case agentMessage of
|
||||
CONF confId connInfo -> do
|
||||
withStore $ \st -> updateConnectionStatus st conn ConnRequested
|
||||
ChatMessage {chatMsgEvent} <- liftEither $ parseChatMessage connInfo
|
||||
case memberCategory m of
|
||||
GCInviteeMember ->
|
||||
case chatMsgEvent of
|
||||
XGrpAcpt memId
|
||||
| memId == memberId m -> do
|
||||
withStore $ \st -> updateGroupMemberStatus st userId (groupMemberId m) GSMemAccepted
|
||||
acceptAgentConnection conn confId XOk
|
||||
| otherwise -> messageError "x.grp.acpt: memberId is different from expected"
|
||||
_ -> messageError "CONF from invited member must have x.grp.acpt"
|
||||
_ ->
|
||||
case chatMsgEvent of
|
||||
XGrpMemInfo memId _memProfile
|
||||
| memId == memberId m -> do
|
||||
-- TODO update member profile
|
||||
Group {membership} <- withStore $ \st -> getGroup st user gName
|
||||
acceptAgentConnection conn confId $ XGrpMemInfo (memberId membership) profile
|
||||
| otherwise -> messageError "x.grp.mem.info: memberId is different from expected"
|
||||
_ -> messageError "CONF from member must have x.grp.mem.info"
|
||||
INFO connInfo -> do
|
||||
withStore $ \st -> updateConnectionStatus st conn ConnSndReady
|
||||
ChatMessage {chatMsgEvent} <- liftEither $ parseChatMessage connInfo
|
||||
case chatMsgEvent of
|
||||
XGrpMemInfo memId _memProfile
|
||||
| memId == memberId m -> do
|
||||
-- TODO update member profile
|
||||
pure ()
|
||||
| otherwise -> messageError "x.grp.mem.info: memberId is different from expected"
|
||||
XOk -> pure ()
|
||||
_ -> messageError "INFO from member must have x.grp.mem.info"
|
||||
pure ()
|
||||
CON -> do
|
||||
group@Group {members, membership} <- withStore $ \st -> getGroup st user gName
|
||||
withStore $ \st -> do
|
||||
updateConnectionStatus st conn ConnReady
|
||||
updateGroupMemberStatus st userId (groupMemberId m) GSMemConnected
|
||||
updateGroupMemberStatus st userId (groupMemberId membership) GSMemConnected
|
||||
-- TODO forward any pending (GMIntroInvReceived) introductions
|
||||
case memberCategory m of
|
||||
GCHostMember -> do
|
||||
showUserJoinedGroup gName
|
||||
setActive $ ActiveG gName
|
||||
showToast ("#" <> gName) "you are connected to group"
|
||||
GCInviteeMember -> do
|
||||
showJoinedGroupMember gName m
|
||||
setActive $ ActiveG gName
|
||||
showToast ("#" <> gName) $ "member " <> localDisplayName (m :: GroupMember) <> " is connected"
|
||||
intros <- withStore $ \st -> createIntroductions st group m
|
||||
sendGroupMessage members . XGrpMemNew $ memberInfo m
|
||||
forM_ intros $ \intro -> do
|
||||
sendDirectMessage agentConnId . XGrpMemIntro . memberInfo $ reMember intro
|
||||
withStore $ \st -> updateIntroStatus st intro GMIntroSent
|
||||
_ -> do
|
||||
if Just (invitedBy membership) == (IBContact <$> memberContactId m)
|
||||
then do
|
||||
-- sender invited the current user
|
||||
withStore $ \st -> updateGroupMemberStatus st userId (groupMemberId m) GSMemConnected
|
||||
showUserConnectedToGroup gName
|
||||
pure ()
|
||||
else do
|
||||
showConnectedGroupMember gName $ displayName (memberProfile m :: Profile)
|
||||
-- TODO send probe and decide whether to use existing contact connection or the new contact connection
|
||||
-- TODO notify member who forwarded introduction - question - where it is stored? There is via_contact but probably there should be via_member in group_members table
|
||||
withStore (\st -> getViaGroupContact st user m) >>= \case
|
||||
Nothing -> do
|
||||
notifyMemberConnected gName m
|
||||
messageError "implementation error: connected member does not have contact"
|
||||
Just ct ->
|
||||
when (contactIsReady ct) $ notifyMemberConnected gName m
|
||||
MSG meta msgBody -> do
|
||||
ChatMessage {chatMsgEvent} <- liftEither $ parseChatMessage msgBody
|
||||
case chatMsgEvent of
|
||||
XGrpMemNew memId memRole memProfile -> do
|
||||
Group {membership, members} <- withStore $ \st -> getGroup st user gName
|
||||
when (memberId membership /= memId && isNothing (find ((== memId) . memberId . fst) members)) $
|
||||
withStore $ \st -> pure () -- add new member as GSMemAccepted
|
||||
_ -> pure ()
|
||||
_ -> pure ()
|
||||
XMsgNew (MsgContent MTText [] body) ->
|
||||
newGroupTextMessage gName m meta $ find (isSimplexContentType XCText) body
|
||||
XGrpMemNew memInfo@(MemberInfo memId _ _) -> do
|
||||
group@Group {membership} <- withStore $ \st -> getGroup st user gName
|
||||
when (memberId membership /= memId) $
|
||||
if isMember memId group
|
||||
then messageError "x.grp.mem.new error: member already exists"
|
||||
else do
|
||||
newMember <- withStore $ \st -> createNewGroupMember st user group memInfo GCPostMember GSMemAnnounced
|
||||
showJoinedGroupMemberConnecting gName m newMember
|
||||
XGrpMemIntro memInfo@(MemberInfo memId _ _) ->
|
||||
case memberCategory m of
|
||||
GCHostMember -> do
|
||||
group <- withStore $ \st -> getGroup st user gName
|
||||
if isMember memId group
|
||||
then messageWarning "x.grp.mem.intro ignored: member already exists"
|
||||
else do
|
||||
(groupConnId, groupQInfo) <- withAgent createConnection
|
||||
(directConnId, directQInfo) <- withAgent createConnection
|
||||
newMember <- withStore $ \st -> createIntroReMember st user group m memInfo groupConnId directConnId
|
||||
let msg = XGrpMemInv memId IntroInvitation {groupQInfo, directQInfo}
|
||||
sendDirectMessage agentConnId msg
|
||||
withStore $ \st -> updateGroupMemberStatus st userId (groupMemberId newMember) GSMemIntroInvited
|
||||
_ -> messageError "x.grp.mem.intro can be only sent by host member"
|
||||
XGrpMemInv memId introInv ->
|
||||
case memberCategory m of
|
||||
GCInviteeMember -> do
|
||||
group <- withStore $ \st -> getGroup st user gName
|
||||
case find ((== memId) . memberId) $ members group of
|
||||
Nothing -> messageError "x.grp.mem.inv error: referenced member does not exists"
|
||||
Just reMember -> do
|
||||
intro <- withStore $ \st -> saveIntroInvitation st reMember m introInv
|
||||
case activeConn (reMember :: GroupMember) of
|
||||
Nothing -> pure () -- this is not an error, introduction will be forwarded once the member is connected
|
||||
Just Connection {agentConnId = reAgentConnId} -> do
|
||||
sendDirectMessage reAgentConnId $ XGrpMemFwd (memberInfo m) introInv
|
||||
withStore $ \st -> updateIntroStatus st intro GMIntroInvForwarded
|
||||
_ -> messageError "x.grp.mem.inv can be only sent by invitee member"
|
||||
XGrpMemFwd memInfo@(MemberInfo memId _ _) introInv@IntroInvitation {groupQInfo, directQInfo} -> do
|
||||
group@Group {membership} <- withStore $ \st -> getGroup st user gName
|
||||
toMember <- case find ((== memId) . memberId) $ members group of
|
||||
-- TODO if the missed messages are correctly sent as soon as there is connection before anything else is sent
|
||||
-- the situation when member does not exist is an error
|
||||
-- member receiving x.grp.mem.fwd should have also received x.grp.mem.new prior to that.
|
||||
-- For now, this branch compensates for the lack of delayed message delivery.
|
||||
Nothing -> withStore $ \st -> createNewGroupMember st user group memInfo GCPostMember GSMemAnnounced
|
||||
Just m' -> pure m'
|
||||
withStore $ \st -> saveMemberInvitation st toMember introInv
|
||||
let msg = XGrpMemInfo (memberId membership) profile
|
||||
groupConnId <- withAgent $ \a -> joinConnection a groupQInfo $ directMessage msg
|
||||
directConnId <- withAgent $ \a -> joinConnection a directQInfo $ directMessage msg
|
||||
withStore $ \st -> createIntroToMemberContact st userId m toMember groupConnId directConnId
|
||||
_ -> messageError $ "unsupported message: " <> T.pack (show chatMsgEvent)
|
||||
_ -> messageError $ "unsupported agent event: " <> T.pack (show agentMessage)
|
||||
where
|
||||
newTextMessage :: ContactName -> MsgMeta -> Maybe MsgBodyContent -> m ()
|
||||
isMember :: MemberId -> Group -> Bool
|
||||
isMember memId Group {membership, members} =
|
||||
memberId membership == memId || isJust (find ((== memId) . memberId) members)
|
||||
|
||||
contactIsReady :: Contact -> Bool
|
||||
contactIsReady Contact {activeConn} = connStatus activeConn == ConnReady
|
||||
|
||||
memberIsReady :: GroupMember -> Bool
|
||||
memberIsReady GroupMember {activeConn} = maybe False ((== ConnReady) . connStatus) activeConn
|
||||
|
||||
notifyMemberConnected :: GroupName -> GroupMember -> m ()
|
||||
notifyMemberConnected gName m@GroupMember {localDisplayName} = do
|
||||
showConnectedToGroupMember gName m
|
||||
setActive $ ActiveG gName
|
||||
showToast ("#" <> gName) $ "member " <> localDisplayName <> " is connected"
|
||||
|
||||
messageWarning :: Text -> m ()
|
||||
messageWarning = liftIO . print
|
||||
|
||||
messageError :: Text -> m ()
|
||||
messageError = liftIO . print
|
||||
|
||||
newTextMessage :: ContactName -> MsgMeta -> Maybe MsgContentBody -> m ()
|
||||
newTextMessage c meta = \case
|
||||
Just MsgBodyContent {contentData = bs} -> do
|
||||
Just MsgContentBody {contentData = bs} -> do
|
||||
let text = safeDecodeUtf8 bs
|
||||
showReceivedMessage c (snd $ broker meta) text (integrity meta)
|
||||
showToast ("@" <> c) text
|
||||
showToast (c <> "> ") text
|
||||
setActive $ ActiveC c
|
||||
_ -> pure ()
|
||||
_ -> messageError "x.msg.new: no expected message body"
|
||||
|
||||
saveGroupInvitation :: Contact -> GroupInvitation -> m ()
|
||||
saveGroupInvitation ct@Contact {localDisplayName} inv@(GroupInvitation (fromMemId, fromRole) (memId, memRole) _ _) = do
|
||||
when (fromRole < GRAdmin || fromRole < memRole) $ throwError $ ChatError CEGroupRole
|
||||
newGroupTextMessage :: GroupName -> GroupMember -> MsgMeta -> Maybe MsgContentBody -> m ()
|
||||
newGroupTextMessage gName GroupMember {localDisplayName = c} meta = \case
|
||||
Just MsgContentBody {contentData = bs} -> do
|
||||
let text = safeDecodeUtf8 bs
|
||||
showReceivedGroupMessage gName c (snd $ broker meta) text (integrity meta)
|
||||
showToast ("#" <> gName <> " " <> c <> "> ") text
|
||||
setActive $ ActiveG gName
|
||||
_ -> messageError "x.msg.new: no expected message body"
|
||||
|
||||
processGroupInvitation :: Contact -> GroupInvitation -> m ()
|
||||
processGroupInvitation ct@Contact {localDisplayName} inv@(GroupInvitation (fromMemId, fromRole) (memId, memRole) _ _) = do
|
||||
when (fromRole < GRAdmin || fromRole < memRole) . throwError . ChatError $ CEGroupContactRole localDisplayName
|
||||
when (fromMemId == memId) $ throwError $ ChatError CEGroupDuplicateMemberId
|
||||
group <- withStore $ \st -> createGroupInvitation st user ct inv
|
||||
showReceivedGroupInvitation group localDisplayName
|
||||
showReceivedGroupInvitation group localDisplayName memRole
|
||||
|
||||
parseChatMessage :: ByteString -> Either ChatError ChatMessage
|
||||
parseChatMessage msgBody = first ChatErrorMessage (parseAll rawChatMessageP msgBody >>= toChatMessage)
|
||||
@@ -306,8 +455,18 @@ directMessage chatMsgEvent =
|
||||
serializeRawChatMessage $
|
||||
rawChatMessage ChatMessage {chatMsgId = Nothing, chatMsgEvent, chatDAG = Nothing}
|
||||
|
||||
sendGroupMessage :: ChatMonad m => [(GroupMember, Maybe Connection)] -> ChatMsgEvent -> m ()
|
||||
sendGroupMessage _members _chatMsgEvent = pure ()
|
||||
sendGroupMessage :: ChatMonad m => [GroupMember] -> ChatMsgEvent -> m ()
|
||||
sendGroupMessage members chatMsgEvent = do
|
||||
let msg = directMessage chatMsgEvent
|
||||
-- TODO once scheduled delivery is implemented memberActive should be changed to memberCurrent
|
||||
withAgent $ \a ->
|
||||
forM_ (filter memberActive members) $
|
||||
traverse (\connId -> sendMessage a connId msg) . memberConnId
|
||||
|
||||
acceptAgentConnection :: ChatMonad m => Connection -> ConfirmationId -> ChatMsgEvent -> m ()
|
||||
acceptAgentConnection conn@Connection {agentConnId} confId msg = do
|
||||
withAgent $ \a -> allowConnection a agentConnId confId $ directMessage msg
|
||||
withStore $ \st -> updateConnectionStatus st conn ConnAccepted
|
||||
|
||||
getCreateActiveUser :: SQLiteStore -> IO User
|
||||
getCreateActiveUser st = do
|
||||
@@ -393,9 +552,9 @@ withStore action =
|
||||
chatCommandP :: Parser ChatCommand
|
||||
chatCommandP =
|
||||
("/help" <|> "/h") $> ChatHelp
|
||||
<|> ("/group #" <|> "/g #") *> (NewGroup <$> groupProfile)
|
||||
<|> ("/add #" <|> "/a #") *> (AddMember <$> displayName <* A.space <*> displayName <*> memberRole)
|
||||
<|> ("/join #" <|> "/j #") *> (JoinGroup <$> displayName)
|
||||
<|> ("/group #" <|> "/group " <|> "/g #" <|> "/g ") *> (NewGroup <$> groupProfile)
|
||||
<|> ("/add #" <|> "/add " <|> "/a #" <|> "/a ") *> (AddMember <$> displayName <* A.space <*> displayName <*> memberRole)
|
||||
<|> ("/join #" <|> "/join " <|> "/j #" <|> "/j ") *> (JoinGroup <$> displayName)
|
||||
<|> ("/remove #" <|> "/rm #") *> (RemoveMember <$> displayName <* A.space <*> displayName)
|
||||
<|> ("/delete #" <|> "/d #") *> (DeleteGroup <$> displayName)
|
||||
<|> ("/members #" <|> "/ms #") *> (ListMembers <$> displayName)
|
||||
@@ -416,4 +575,4 @@ chatCommandP =
|
||||
(" owner" $> GROwner)
|
||||
<|> (" admin" $> GRAdmin)
|
||||
<|> (" normal" $> GRMember)
|
||||
<|> pure GRMember
|
||||
<|> pure GRAdmin
|
||||
|
||||
Reference in New Issue
Block a user