{-# LANGUAGE DataKinds #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE KindSignatures #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE StandaloneDeriving #-} module Directory.Events ( DirectoryEvent (..), DirectoryCmd (..), DirectoryCmdTag (..), ADirectoryCmd (..), DirectoryHelpSection (..), DirectoryRole (..), SDirectoryRole (..), crDirectoryEvent, directoryCmdP, directoryCmdTag, ) where import Control.Applicative (optional, (<|>)) import Data.Attoparsec.Text (Parser) import qualified Data.Attoparsec.Text as A import Data.Char (isSpace) import Data.Either (fromRight) import Data.Functor (($>)) import Data.Maybe (fromMaybe) import Data.Text (Text) import qualified Data.Text as T import Data.Text.Encoding (encodeUtf8) import Directory.Store import Simplex.Chat.Controller import Simplex.Chat.Markdown (MarkdownList, displayNameTextP) import Simplex.Chat.Messages import Simplex.Chat.Messages.CIContent import Simplex.Chat.Protocol (LinkOwnerSig, MsgChatLink, MsgContent (..)) import Simplex.Chat.Types import Simplex.Chat.Types.Shared import Simplex.Messaging.Agent.Protocol (AgentErrorType (..)) import Simplex.Messaging.Encoding.String import Simplex.Messaging.Protocol (BrokerErrorType (..)) import Simplex.Messaging.Util (tshow, (<$?>)) data DirectoryEvent = DEContactConnected Contact | DEGroupInvitation {contact :: Contact, groupInfo :: GroupInfo, fromMemberRole :: GroupMemberRole, memberRole :: GroupMemberRole} | DEServiceJoinedGroup {contactId :: ContactId, groupInfo :: GroupInfo, hostMember :: GroupMember} | DEGroupUpdated {member :: GroupMember, fromGroup :: GroupInfo, toGroup :: GroupInfo} | DEGroupLinkCheck GroupInfo | DEPendingMember GroupInfo GroupMember | DEPendingMemberMsg GroupInfo GroupMember ChatItemId Text | DEContactRoleChanged GroupInfo ContactId GroupMemberRole -- contactId here is the contact whose role changed | DEServiceRoleChanged GroupInfo GroupMemberRole | DEContactRemovedFromGroup ContactId GroupInfo | DEContactLeftGroup ContactId GroupInfo | DEServiceRemovedFromGroup GroupInfo | DEGroupDeleted GroupInfo | DEChatLinkReceived {contact :: Contact, chatItemId :: ChatItemId, chatLink :: MsgChatLink, ownerSig :: Maybe LinkOwnerSig} | DEMemberUpdated {groupInfo :: GroupInfo, fromMember :: GroupMember, toMember :: GroupMember} | DEUnsupportedMessage Contact ChatItemId | DEItemEditIgnored Contact | DEItemDeleteIgnored Contact | DEContactCommand Contact ChatItemId ADirectoryCmd | DELogChatResponse Text deriving (Show) crDirectoryEvent :: Either ChatError ChatEvent -> Maybe DirectoryEvent crDirectoryEvent = \case Right evt -> crDirectoryEvent_ evt Left e -> case e of ChatErrorAgent {agentError = BROKER _ (NETWORK _)} -> Nothing ChatErrorAgent {agentError = BROKER _ TIMEOUT} -> Nothing _ -> Just $ DELogChatResponse $ "chat error: " <> tshow e crDirectoryEvent_ :: ChatEvent -> Maybe DirectoryEvent crDirectoryEvent_ = \case CEvtContactConnected {contact} -> Just $ DEContactConnected contact CEvtReceivedGroupInvitation {contact, groupInfo, fromMemberRole, memberRole} -> Just $ DEGroupInvitation {contact, groupInfo, fromMemberRole, memberRole} CEvtUserJoinedGroup {groupInfo, hostMember} -> (\contactId -> DEServiceJoinedGroup {contactId, groupInfo, hostMember}) <$> memberContactId hostMember CEvtGroupUpdated {fromGroup, toGroup, member_} -> (\member -> DEGroupUpdated {member, fromGroup, toGroup}) <$> member_ CEvtJoinedGroupMember {groupInfo, member = m} | pending m -> Just $ DEPendingMember groupInfo m | otherwise -> Nothing CEvtNewChatItems {chatItems = AChatItem _ _ (GroupChat g _scopeInfo) ci : _} -> case ci of ChatItem {chatDir = CIGroupRcv m, content = CIRcvMsgContent (MCText t)} | pending m -> Just $ DEPendingMemberMsg g m (chatItemId' ci) t _ -> Nothing CEvtMemberRole {groupInfo, member, toRole} | groupMemberId' member == groupMemberId' (membership groupInfo) -> Just $ DEServiceRoleChanged groupInfo toRole | otherwise -> (\ctId -> DEContactRoleChanged groupInfo ctId toRole) <$> memberContactId member CEvtDeletedMember {groupInfo, deletedMember} -> (`DEContactRemovedFromGroup` groupInfo) <$> memberContactId deletedMember CEvtLeftMember {groupInfo, member} -> (`DEContactLeftGroup` groupInfo) <$> memberContactId member CEvtDeletedMemberUser {groupInfo} -> Just $ DEServiceRemovedFromGroup groupInfo CEvtGroupDeleted {groupInfo} -> Just $ DEGroupDeleted groupInfo CEvtUnknownMemberAnnounced {groupInfo, unknownMember, announcedMember} -> Just $ DEMemberUpdated {groupInfo, fromMember = unknownMember, toMember = announcedMember} CEvtGroupMemberUpdated {groupInfo, fromMember, toMember} -> Just $ DEMemberUpdated {groupInfo, fromMember, toMember} CEvtChatItemUpdated {chatItem = AChatItem _ SMDRcv (DirectChat ct) _} -> Just $ DEItemEditIgnored ct CEvtChatItemsDeleted {chatItemDeletions = ((ChatItemDeletion (AChatItem _ SMDRcv (DirectChat ct) _) _) : _), byUser = False} -> Just $ DEItemDeleteIgnored ct CEvtNewChatItems {chatItems = (AChatItem _ SMDRcv (DirectChat ct) ci@ChatItem {content = CIRcvMsgContent mc, formattedText = ft, meta = CIMeta {itemLive}}) : _} -> Just $ case (mc, itemLive) of (MCText t, Nothing) -> DEContactCommand ct ciId $ fromRight err $ A.parseOnly (directoryCmdP ft <* A.endOfInput) $ T.dropWhileEnd isSpace t (MCChat {chatLink, ownerSig}, Nothing) -> DEChatLinkReceived {contact = ct, chatItemId = ciId, chatLink, ownerSig} _ -> DEUnsupportedMessage ct ciId where ciId = chatItemId' ci err = ADC SDRUser DCUnknownCommand CEvtMessageError {severity, errorMessage} -> Just $ DELogChatResponse $ "message error: " <> severity <> ", " <> errorMessage CEvtChatErrors {chatErrors} -> Just $ DELogChatResponse $ "chat errors: " <> T.intercalate ", " (map tshow chatErrors) _ -> Nothing where pending m = memberStatus m == GSMemPendingApproval data DirectoryRole = DRUser | DRAdmin | DRSuperUser data SDirectoryRole (r :: DirectoryRole) where SDRUser :: SDirectoryRole 'DRUser SDRAdmin :: SDirectoryRole 'DRAdmin SDRSuperUser :: SDirectoryRole 'DRSuperUser deriving instance Show (SDirectoryRole r) data DirectoryCmdTag (r :: DirectoryRole) where DCHelp_ :: DirectoryCmdTag 'DRUser DCSearchNext_ :: DirectoryCmdTag 'DRUser DCAllGroups_ :: DirectoryCmdTag 'DRUser DCRecentGroups_ :: DirectoryCmdTag 'DRUser DCSubmitGroup_ :: DirectoryCmdTag 'DRUser DCConfirmDuplicateGroup_ :: DirectoryCmdTag 'DRUser DCListUserGroups_ :: DirectoryCmdTag 'DRUser DCDeleteGroup_ :: DirectoryCmdTag 'DRUser DCMemberRole_ :: DirectoryCmdTag 'DRUser DCGroupFilter_ :: DirectoryCmdTag 'DRUser DCShowUpgradeGroupLink_ :: DirectoryCmdTag 'DRUser DCApproveGroup_ :: DirectoryCmdTag 'DRAdmin DCRejectGroup_ :: DirectoryCmdTag 'DRAdmin DCSuspendGroup_ :: DirectoryCmdTag 'DRAdmin DCResumeGroup_ :: DirectoryCmdTag 'DRAdmin DCListLastGroups_ :: DirectoryCmdTag 'DRAdmin DCListPendingGroups_ :: DirectoryCmdTag 'DRAdmin DCSendToGroupOwner_ :: DirectoryCmdTag 'DRAdmin DCInviteOwnerToGroup_ :: DirectoryCmdTag 'DRAdmin -- DCAddBlockedWord_ :: DirectoryCmdTag 'DRAdmin -- DCRemoveBlockedWord_ :: DirectoryCmdTag 'DRAdmin DCPromoteGroup_ :: DirectoryCmdTag 'DRSuperUser DCExecuteCommand_ :: DirectoryCmdTag 'DRSuperUser deriving instance Show (DirectoryCmdTag r) data ADirectoryCmdTag = forall r. ADCT (SDirectoryRole r) (DirectoryCmdTag r) data DirectoryHelpSection = DHSRegistration | DHSCommands deriving (Show) data DirectoryCmd (r :: DirectoryRole) where DCHelp :: DirectoryHelpSection -> DirectoryCmd 'DRUser DCSearchGroup :: Text -> Maybe MarkdownList -> DirectoryCmd 'DRUser DCSearchNext :: DirectoryCmd 'DRUser DCAllGroups :: DirectoryCmd 'DRUser DCRecentGroups :: DirectoryCmd 'DRUser DCSubmitGroup :: ConnReqContact -> DirectoryCmd 'DRUser DCConfirmDuplicateGroup :: UserGroupRegId -> GroupName -> DirectoryCmd 'DRUser DCListUserGroups :: DirectoryCmd 'DRUser DCDeleteGroup :: UserGroupRegId -> GroupName -> DirectoryCmd 'DRUser DCMemberRole :: UserGroupRegId -> Maybe GroupName -> Maybe GroupMemberRole -> DirectoryCmd 'DRUser DCGroupFilter :: UserGroupRegId -> Maybe GroupName -> Maybe DirectoryMemberAcceptance -> DirectoryCmd 'DRUser DCShowUpgradeGroupLink :: GroupId -> Maybe GroupName -> DirectoryCmd 'DRUser DCApproveGroup :: {groupId :: GroupId, displayName :: GroupName, groupApprovalId :: GroupApprovalId, promote :: Maybe Bool} -> DirectoryCmd 'DRAdmin DCRejectGroup :: GroupId -> GroupName -> DirectoryCmd 'DRAdmin DCSuspendGroup :: GroupId -> GroupName -> DirectoryCmd 'DRAdmin DCResumeGroup :: GroupId -> GroupName -> DirectoryCmd 'DRAdmin DCListLastGroups :: Int -> DirectoryCmd 'DRAdmin DCListPendingGroups :: Int -> DirectoryCmd 'DRAdmin DCSendToGroupOwner :: GroupId -> GroupName -> Text -> DirectoryCmd 'DRAdmin DCInviteOwnerToGroup :: GroupId -> GroupName -> DirectoryCmd 'DRAdmin -- DCAddBlockedWord :: Text -> DirectoryCmd 'DRAdmin -- DCRemoveBlockedWord :: Text -> DirectoryCmd 'DRAdmin DCPromoteGroup :: GroupId -> GroupName -> Bool -> DirectoryCmd 'DRSuperUser DCExecuteCommand :: String -> DirectoryCmd 'DRSuperUser DCUnknownCommand :: DirectoryCmd 'DRUser DCCommandError :: DirectoryCmdTag r -> DirectoryCmd r deriving instance Show (DirectoryCmd r) data ADirectoryCmd = forall r. ADC (SDirectoryRole r) (DirectoryCmd r) deriving instance Show ADirectoryCmd directoryCmdP :: Maybe MarkdownList -> Parser ADirectoryCmd directoryCmdP ft = (A.char '/' *> cmdStrP) <|> (A.char '.' $> ADC SDRUser DCSearchNext) <|> (ADC SDRUser . (`DCSearchGroup` ft) <$> A.takeText) where cmdStrP = (tagP >>= \(ADCT u t) -> ADC u <$> (cmdP t <|> pure (DCCommandError t))) <|> pure (ADC SDRUser DCUnknownCommand) tagP = A.takeTill isSpace >>= \case "help" -> u DCHelp_ "h" -> u DCHelp_ "next" -> u DCSearchNext_ "all" -> u DCAllGroups_ "new" -> u DCRecentGroups_ "submit" -> u DCSubmitGroup_ "confirm" -> u DCConfirmDuplicateGroup_ "list" -> u DCListUserGroups_ "ls" -> u DCListUserGroups_ "delete" -> u DCDeleteGroup_ "role" -> u DCMemberRole_ "filter" -> u DCGroupFilter_ "link" -> u DCShowUpgradeGroupLink_ "approve" -> au DCApproveGroup_ "reject" -> au DCRejectGroup_ "suspend" -> au DCSuspendGroup_ "resume" -> au DCResumeGroup_ "last" -> au DCListLastGroups_ "pending" -> au DCListPendingGroups_ "owner" -> au DCSendToGroupOwner_ "invite" -> au DCInviteOwnerToGroup_ -- "block_word" -> au DCAddBlockedWord_ -- "unblock_word" -> au DCRemoveBlockedWord_ "promote" -> su DCPromoteGroup_ "exec" -> su DCExecuteCommand_ "x" -> su DCExecuteCommand_ _ -> fail "bad command tag" where u = pure . ADCT SDRUser au = pure . ADCT SDRAdmin su = pure . ADCT SDRSuperUser cmdP :: DirectoryCmdTag r -> Parser (DirectoryCmd r) cmdP = \case DCHelp_ -> DCHelp . fromMaybe DHSRegistration <$> optional (A.takeWhile isSpace *> helpSectionP) where helpSectionP = A.takeText >>= \case "registration" -> pure DHSRegistration "r" -> pure DHSRegistration "commands" -> pure DHSCommands "c" -> pure DHSCommands _ -> fail "bad help section" DCSearchNext_ -> pure DCSearchNext DCAllGroups_ -> pure DCAllGroups DCRecentGroups_ -> pure DCRecentGroups DCSubmitGroup_ -> fmap DCSubmitGroup . strDecode . encodeUtf8 <$?> (spacesP *> A.takeText) DCConfirmDuplicateGroup_ -> gc DCConfirmDuplicateGroup DCListUserGroups_ -> pure DCListUserGroups DCDeleteGroup_ -> gc DCDeleteGroup DCMemberRole_ -> do (groupId, displayName_) <- gc_ (,) memberRole_ <- optional $ spacesP *> ("member" $> GRMember <|> "observer" $> GRObserver) pure $ DCMemberRole groupId displayName_ memberRole_ DCGroupFilter_ -> do (groupId, displayName_) <- gc_ (,) acceptance_ <- (A.takeWhile isSpace >> A.endOfInput) $> Nothing <|> Just <$> (acceptancePresetsP <|> acceptanceFiltersP) pure $ DCGroupFilter groupId displayName_ acceptance_ where acceptancePresetsP = spacesP *> A.choice [ "off" $> noJoinFilter, "basic" $> basicJoinFilter, ("moderate" <|> "mod") $> moderateJoinFilter, "strong" $> strongJoinFilter ] acceptanceFiltersP = do rejectNames <- filterP "name" passCaptcha <- filterP "captcha" makeObserver <- filterP "observer" pure DirectoryMemberAcceptance {rejectNames, passCaptcha, makeObserver} filterP :: Text -> Parser (Maybe ProfileCondition) filterP s = Just <$> (spacesP *> A.string s *> conditionP) <|> pure Nothing conditionP = "=all" $> PCAll <|> ("=noimage" <|> "=no_image" <|> "=no-image") $> PCNoImage <|> pure PCAll DCShowUpgradeGroupLink_ -> gc_ DCShowUpgradeGroupLink DCApproveGroup_ -> do (groupId, displayName) <- gc (,) groupApprovalId <- A.space *> A.decimal promote <- Just <$> (" promote=" *> onOffP) <|> pure Nothing pure DCApproveGroup {groupId, displayName, groupApprovalId, promote} DCRejectGroup_ -> gc DCRejectGroup DCSuspendGroup_ -> gc DCSuspendGroup DCResumeGroup_ -> gc DCResumeGroup DCListLastGroups_ -> DCListLastGroups <$> (A.space *> A.decimal <|> pure 10) DCListPendingGroups_ -> DCListPendingGroups <$> (A.space *> A.decimal <|> pure 10) DCSendToGroupOwner_ -> do (groupId, displayName) <- gc (,) msg <- A.space *> A.takeText pure $ DCSendToGroupOwner groupId displayName msg DCInviteOwnerToGroup_ -> gc DCInviteOwnerToGroup -- DCAddBlockedWord_ -> DCAddBlockedWord <$> wordP -- DCRemoveBlockedWord_ -> DCRemoveBlockedWord <$> wordP DCPromoteGroup_ -> do (groupId, displayName) <- gc (,) promote <- A.space *> onOffP pure $ DCPromoteGroup groupId displayName promote DCExecuteCommand_ -> DCExecuteCommand . T.unpack <$> (spacesP *> A.takeText) where gc f = f <$> (spacesP *> A.decimal) <*> (A.char ':' *> displayNameTextP) gc_ f = f <$> (spacesP *> A.decimal) <*> optional (A.char ':' *> displayNameTextP) -- wordP = spacesP *> A.takeTill isSpace spacesP = A.takeWhile1 isSpace onOffP = (A.string "on" $> True) <|> (A.string "off" $> False) directoryCmdTag :: DirectoryCmd r -> Text directoryCmdTag = \case DCHelp _ -> "help" DCSearchGroup {} -> "search" DCSearchNext -> "next" DCAllGroups -> "all" DCRecentGroups -> "new" DCSubmitGroup _ -> "submit" DCConfirmDuplicateGroup {} -> "confirm" DCListUserGroups -> "list" DCDeleteGroup {} -> "delete" DCApproveGroup {} -> "approve" DCMemberRole {} -> "role" DCGroupFilter {} -> "filter" DCShowUpgradeGroupLink {} -> "link" DCRejectGroup {} -> "reject" DCSuspendGroup {} -> "suspend" DCResumeGroup {} -> "resume" DCListLastGroups _ -> "last" DCListPendingGroups _ -> "pending" DCSendToGroupOwner {} -> "owner" DCInviteOwnerToGroup {} -> "invite" -- DCAddBlockedWord _ -> "block_word" -- DCRemoveBlockedWord _ -> "unblock_word" DCPromoteGroup {} -> "promote" DCExecuteCommand _ -> "exec" DCUnknownCommand -> "unknown" DCCommandError _ -> "error"