2021-07-06 19:07:03 +01:00
|
|
|
{-# LANGUAGE DataKinds #-}
|
2021-07-04 18:42:24 +01:00
|
|
|
{-# LANGUAGE DuplicateRecordFields #-}
|
2021-06-25 18:18:24 +01:00
|
|
|
{-# LANGUAGE FlexibleContexts #-}
|
|
|
|
|
{-# LANGUAGE GADTs #-}
|
|
|
|
|
{-# LANGUAGE LambdaCase #-}
|
|
|
|
|
{-# LANGUAGE NamedFieldPuns #-}
|
|
|
|
|
{-# LANGUAGE OverloadedStrings #-}
|
2021-07-04 18:42:24 +01:00
|
|
|
{-# LANGUAGE RankNTypes #-}
|
|
|
|
|
{-# LANGUAGE ScopedTypeVariables #-}
|
2021-06-25 18:18:24 +01:00
|
|
|
|
|
|
|
|
module Simplex.Chat where
|
|
|
|
|
|
2021-07-05 19:54:44 +01:00
|
|
|
import Control.Applicative ((<|>))
|
2021-07-07 22:46:38 +01:00
|
|
|
import Control.Logger.Simple
|
2021-06-25 18:18:24 +01:00
|
|
|
import Control.Monad.Except
|
|
|
|
|
import Control.Monad.IO.Unlift
|
|
|
|
|
import Control.Monad.Reader
|
2021-07-11 12:22:22 +01:00
|
|
|
import Data.Attoparsec.ByteString.Char8 (Parser, (<?>))
|
2021-06-25 18:18:24 +01:00
|
|
|
import qualified Data.Attoparsec.ByteString.Char8 as A
|
|
|
|
|
import Data.Bifunctor (first)
|
|
|
|
|
import Data.ByteString.Char8 (ByteString)
|
2021-07-11 12:22:22 +01:00
|
|
|
import qualified Data.ByteString.Char8 as B
|
2021-06-25 18:18:24 +01:00
|
|
|
import Data.Functor (($>))
|
2021-07-04 18:42:24 +01:00
|
|
|
import Data.List (find)
|
2021-07-05 19:54:44 +01:00
|
|
|
import Data.Maybe (isJust)
|
2021-07-04 18:42:24 +01:00
|
|
|
import Data.Text (Text)
|
2021-06-25 18:18:24 +01:00
|
|
|
import qualified Data.Text as T
|
|
|
|
|
import Data.Text.Encoding (encodeUtf8)
|
|
|
|
|
import Simplex.Chat.Controller
|
2021-07-05 20:05:07 +01:00
|
|
|
import Simplex.Chat.Help
|
2021-07-07 22:46:38 +01:00
|
|
|
import Simplex.Chat.Input
|
2021-07-05 20:05:07 +01:00
|
|
|
import Simplex.Chat.Notification
|
2021-07-07 22:46:38 +01:00
|
|
|
import Simplex.Chat.Options (ChatOpts (..))
|
2021-07-04 18:42:24 +01:00
|
|
|
import Simplex.Chat.Protocol
|
2021-07-05 20:05:07 +01:00
|
|
|
import Simplex.Chat.Store
|
2021-06-25 18:18:24 +01:00
|
|
|
import Simplex.Chat.Styled (plain)
|
2021-07-05 20:05:07 +01:00
|
|
|
import Simplex.Chat.Terminal
|
2021-07-04 18:42:24 +01:00
|
|
|
import Simplex.Chat.Types
|
2021-07-05 20:05:07 +01:00
|
|
|
import Simplex.Chat.View
|
2021-06-25 18:18:24 +01:00
|
|
|
import Simplex.Messaging.Agent
|
2021-07-07 22:46:38 +01:00
|
|
|
import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..))
|
2021-06-25 18:18:24 +01:00
|
|
|
import Simplex.Messaging.Agent.Protocol
|
2021-07-07 22:46:38 +01:00
|
|
|
import Simplex.Messaging.Client (smpDefaultConfig)
|
2021-06-25 18:18:24 +01:00
|
|
|
import Simplex.Messaging.Parsers (parseAll)
|
2021-07-04 18:42:24 +01:00
|
|
|
import Simplex.Messaging.Util (bshow, raceAny_)
|
2021-07-05 19:54:44 +01:00
|
|
|
import System.Exit (exitFailure)
|
|
|
|
|
import System.IO (hFlush, stdout)
|
|
|
|
|
import Text.Read (readMaybe)
|
2021-07-07 22:46:38 +01:00
|
|
|
import UnliftIO.Async (race_)
|
2021-07-04 18:42:24 +01:00
|
|
|
import qualified UnliftIO.Exception as E
|
2021-06-25 18:18:24 +01:00
|
|
|
import UnliftIO.STM
|
|
|
|
|
|
|
|
|
|
data ChatCommand
|
|
|
|
|
= ChatHelp
|
|
|
|
|
| MarkdownHelp
|
2021-07-05 19:54:44 +01:00
|
|
|
| AddContact
|
|
|
|
|
| Connect SMPQueueInfo
|
2021-07-04 18:42:24 +01:00
|
|
|
| DeleteContact ContactRef
|
|
|
|
|
| SendMessage ContactRef ByteString
|
2021-07-11 12:22:22 +01:00
|
|
|
| NewGroup GroupRef
|
|
|
|
|
| AddMember GroupRef ContactRef GroupMemberRole
|
|
|
|
|
| RemoveMember GroupRef ContactRef
|
|
|
|
|
| MemberRole GroupRef ContactRef GroupMemberRole
|
|
|
|
|
| LeaveGroup GroupRef
|
|
|
|
|
| DeleteGroup GroupRef
|
|
|
|
|
| ListMembers GroupRef
|
|
|
|
|
| SendGroupMessage GroupRef ByteString
|
2021-06-25 18:18:24 +01:00
|
|
|
deriving (Show)
|
|
|
|
|
|
2021-07-07 22:46:38 +01:00
|
|
|
cfg :: AgentConfig
|
|
|
|
|
cfg =
|
|
|
|
|
AgentConfig
|
|
|
|
|
{ tcpPort = undefined, -- agent does not listen to TCP
|
|
|
|
|
smpServers = undefined, -- filled in from options
|
|
|
|
|
rsaKeySize = 2048 `div` 8,
|
|
|
|
|
connIdBytes = 12,
|
|
|
|
|
tbqSize = 16,
|
|
|
|
|
dbFile = undefined, -- filled in from options
|
|
|
|
|
dbPoolSize = 4,
|
|
|
|
|
smpCfg = smpDefaultConfig
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logCfg :: LogConfig
|
|
|
|
|
logCfg = LogConfig {lc_file = Nothing, lc_stderr = True}
|
|
|
|
|
|
|
|
|
|
simplexChat :: WithTerminal t => ChatOpts -> t -> IO ()
|
|
|
|
|
simplexChat opts t = do
|
|
|
|
|
-- setLogLevel LogInfo -- LogError
|
|
|
|
|
-- withGlobalLogging logCfg $ do
|
|
|
|
|
initializeNotifications
|
|
|
|
|
>>= newChatController opts t
|
|
|
|
|
>>= runSimplexChat
|
|
|
|
|
|
|
|
|
|
newChatController :: WithTerminal t => ChatOpts -> t -> (Notification -> IO ()) -> IO ChatController
|
|
|
|
|
newChatController ChatOpts {dbFile, smpServers} t sendNotification = do
|
|
|
|
|
chatStore <- createStore (dbFile <> ".chat.db") 4
|
|
|
|
|
currentUser <- getCreateActiveUser chatStore
|
|
|
|
|
chatTerminal <- newChatTerminal t
|
|
|
|
|
smpAgent <- getSMPAgentClient cfg {dbFile = dbFile <> ".agent.db", smpServers}
|
|
|
|
|
inputQ <- newTBQueueIO $ tbqSize cfg
|
|
|
|
|
notifyQ <- newTBQueueIO $ tbqSize cfg
|
|
|
|
|
pure ChatController {currentUser, smpAgent, chatTerminal, chatStore, inputQ, notifyQ, sendNotification}
|
|
|
|
|
|
|
|
|
|
runSimplexChat :: ChatController -> IO ()
|
|
|
|
|
runSimplexChat = runReaderT (race_ runTerminalInput runChatController)
|
|
|
|
|
|
2021-06-25 18:18:24 +01:00
|
|
|
runChatController :: (MonadUnliftIO m, MonadReader ChatController m) => m ()
|
2021-06-26 20:20:33 +01:00
|
|
|
runChatController =
|
|
|
|
|
raceAny_
|
|
|
|
|
[ inputSubscriber,
|
|
|
|
|
agentSubscriber,
|
|
|
|
|
notificationSubscriber
|
|
|
|
|
]
|
2021-06-25 18:18:24 +01:00
|
|
|
|
|
|
|
|
inputSubscriber :: (MonadUnliftIO m, MonadReader ChatController m) => m ()
|
|
|
|
|
inputSubscriber = do
|
|
|
|
|
q <- asks inputQ
|
|
|
|
|
forever $
|
|
|
|
|
atomically (readTBQueue q) >>= \case
|
|
|
|
|
InputControl _ -> pure ()
|
|
|
|
|
InputCommand s ->
|
|
|
|
|
case parseAll chatCommandP . encodeUtf8 $ T.pack s of
|
|
|
|
|
Left e -> printToView [plain s, "invalid input: " <> plain e]
|
|
|
|
|
Right cmd -> do
|
|
|
|
|
case cmd of
|
|
|
|
|
SendMessage c msg -> showSentMessage c msg
|
|
|
|
|
_ -> printToView [plain s]
|
2021-07-05 19:54:44 +01:00
|
|
|
user <- asks currentUser
|
2021-07-06 19:07:03 +01:00
|
|
|
void . runExceptT $ processChatCommand user cmd `catchError` showChatError
|
2021-06-25 18:18:24 +01:00
|
|
|
|
2021-07-05 19:54:44 +01:00
|
|
|
processChatCommand :: ChatMonad m => User -> ChatCommand -> m ()
|
|
|
|
|
processChatCommand User {userId, profile} = \case
|
2021-06-25 18:18:24 +01:00
|
|
|
ChatHelp -> printToView chatHelpInfo
|
|
|
|
|
MarkdownHelp -> printToView markdownInfo
|
2021-07-05 19:54:44 +01:00
|
|
|
AddContact -> do
|
|
|
|
|
(connId, qInfo) <- withAgent createConnection
|
|
|
|
|
withStore $ \st -> createDirectConnection st userId connId
|
|
|
|
|
showInvitation qInfo
|
|
|
|
|
Connect qInfo -> do
|
2021-07-06 19:07:03 +01:00
|
|
|
connId <- withAgent $ \agent -> joinConnection agent qInfo $ encodeProfile profile
|
2021-07-05 19:54:44 +01:00
|
|
|
withStore $ \st -> createDirectConnection st userId connId
|
2021-07-04 18:42:24 +01:00
|
|
|
DeleteContact cRef -> do
|
|
|
|
|
conns <- withStore $ \st -> getContactConnections st userId cRef
|
2021-07-05 19:54:44 +01:00
|
|
|
withAgent $ \smp -> forM_ conns $ \Connection {agentConnId} ->
|
2021-07-04 18:42:24 +01:00
|
|
|
deleteConnection smp agentConnId `catchError` \(_ :: AgentErrorType) -> pure ()
|
2021-07-05 19:54:44 +01:00
|
|
|
withStore $ \st -> deleteContact st userId cRef
|
2021-07-04 18:42:24 +01:00
|
|
|
unsetActive $ ActiveC cRef
|
|
|
|
|
when (null conns) . throwError . ChatErrorContact $ CENotFound cRef
|
|
|
|
|
showContactDeleted cRef
|
|
|
|
|
SendMessage cRef msg -> do
|
|
|
|
|
Connection {agentConnId} <- withStore $ \st -> getContactConnection st userId cRef
|
2021-07-11 12:22:22 +01:00
|
|
|
let body = MsgBodyContent {contentType = SimplexContentType XCText, contentData = msg}
|
|
|
|
|
rawMsg = rawChatMessage ChatMessage {chatMsgId = Nothing, chatMsgEvent = XMsgNew MTText [] [body], chatDAG = Nothing}
|
2021-07-05 19:54:44 +01:00
|
|
|
void . withAgent $ \smp -> sendMessage smp agentConnId $ serializeRawChatMessage rawMsg
|
2021-07-04 18:42:24 +01:00
|
|
|
setActive $ ActiveC cRef
|
2021-07-11 12:22:22 +01:00
|
|
|
NewGroup _gRef -> pure ()
|
|
|
|
|
AddMember _gRef _cRef _mRole -> pure ()
|
|
|
|
|
MemberRole _gRef _cRef _mRole -> pure ()
|
|
|
|
|
RemoveMember _gRef _cRef -> pure ()
|
|
|
|
|
LeaveGroup _gRef -> pure ()
|
|
|
|
|
DeleteGroup _gRef -> pure ()
|
|
|
|
|
ListMembers _gRef -> pure ()
|
|
|
|
|
SendGroupMessage _gRef _msg -> pure ()
|
2021-06-25 18:18:24 +01:00
|
|
|
|
|
|
|
|
agentSubscriber :: (MonadUnliftIO m, MonadReader ChatController m) => m ()
|
|
|
|
|
agentSubscriber = do
|
2021-07-06 19:07:03 +01:00
|
|
|
q <- asks $ subQ . smpAgent
|
2021-06-25 18:18:24 +01:00
|
|
|
forever $ do
|
2021-07-06 19:07:03 +01:00
|
|
|
(_, connId, msg) <- atomically $ readTBQueue q
|
|
|
|
|
user <- asks currentUser
|
|
|
|
|
-- TODO handle errors properly
|
|
|
|
|
void . runExceptT $ processAgentMessage user connId msg `catchError` (liftIO . print)
|
2021-07-04 18:42:24 +01:00
|
|
|
|
2021-07-06 19:07:03 +01:00
|
|
|
processAgentMessage :: forall m. ChatMonad m => User -> ConnId -> ACommand 'Agent -> m ()
|
|
|
|
|
processAgentMessage User {userId, profile} agentConnId agentMessage = do
|
|
|
|
|
chatDirection <- withStore $ \st -> getConnectionChatDirection st userId agentConnId
|
|
|
|
|
case chatDirection of
|
|
|
|
|
ReceivedDirectMessage Contact {localContactRef = c} ->
|
|
|
|
|
case agentMessage of
|
|
|
|
|
MSG meta msgBody -> do
|
2021-07-11 12:22:22 +01:00
|
|
|
ChatMessage {chatMsgEvent} <- liftEither $ parseChatMessage msgBody
|
2021-07-04 18:42:24 +01:00
|
|
|
case chatMsgEvent of
|
2021-07-11 12:22:22 +01:00
|
|
|
XMsgNew MTText [] body -> newTextMessage c meta $ find (isSimplexContentType XCText) body
|
|
|
|
|
XInfo _ -> pure () -- TODO profile update
|
2021-07-04 18:42:24 +01:00
|
|
|
_ -> pure ()
|
2021-07-06 19:07:03 +01:00
|
|
|
CON -> do
|
|
|
|
|
-- TODO update connection status
|
|
|
|
|
showContactConnected c
|
|
|
|
|
showToast ("@" <> c) "connected"
|
|
|
|
|
setActive $ ActiveC c
|
|
|
|
|
END -> do
|
|
|
|
|
showContactDisconnected c
|
|
|
|
|
showToast ("@" <> c) "disconnected"
|
|
|
|
|
unsetActive $ ActiveC c
|
|
|
|
|
_ -> pure ()
|
|
|
|
|
ReceivedDirectMessage NewContact {activeConn} ->
|
|
|
|
|
case agentMessage of
|
|
|
|
|
CONF confId connInfo -> do
|
|
|
|
|
-- TODO update connection status
|
|
|
|
|
saveConnInfo activeConn connInfo
|
|
|
|
|
withAgent $ \a -> allowConnection a agentConnId confId $ encodeProfile profile
|
|
|
|
|
INFO connInfo ->
|
|
|
|
|
saveConnInfo activeConn connInfo
|
|
|
|
|
_ -> pure ()
|
|
|
|
|
_ -> pure ()
|
2021-07-05 19:54:44 +01:00
|
|
|
where
|
2021-07-07 22:46:38 +01:00
|
|
|
newTextMessage :: ContactRef -> MsgMeta -> Maybe MsgBodyContent -> m ()
|
|
|
|
|
newTextMessage c meta = \case
|
2021-07-11 12:22:22 +01:00
|
|
|
Just MsgBodyContent {contentData = bs} -> do
|
2021-07-07 22:46:38 +01:00
|
|
|
let text = safeDecodeUtf8 bs
|
|
|
|
|
showReceivedMessage c (snd $ broker meta) text (integrity meta)
|
|
|
|
|
showToast ("@" <> c) text
|
|
|
|
|
setActive $ ActiveC c
|
|
|
|
|
_ -> pure ()
|
|
|
|
|
|
2021-07-06 19:07:03 +01:00
|
|
|
parseChatMessage :: ByteString -> Either ChatError ChatMessage
|
|
|
|
|
parseChatMessage msgBody = first ChatErrorMessage (parseAll rawChatMessageP msgBody >>= toChatMessage)
|
|
|
|
|
|
|
|
|
|
saveConnInfo :: Connection -> ConnInfo -> m ()
|
|
|
|
|
saveConnInfo activeConn connInfo = do
|
2021-07-11 12:22:22 +01:00
|
|
|
ChatMessage {chatMsgEvent} <- liftEither $ parseChatMessage connInfo
|
2021-07-06 19:07:03 +01:00
|
|
|
case chatMsgEvent of
|
2021-07-11 12:22:22 +01:00
|
|
|
XInfo p ->
|
|
|
|
|
withStore $ \st -> createDirectContact st userId activeConn p
|
2021-07-06 19:07:03 +01:00
|
|
|
_ -> pure () -- TODO show/log error, other events in SMP confirmation
|
|
|
|
|
|
|
|
|
|
encodeProfile :: Profile -> ByteString
|
|
|
|
|
encodeProfile profile =
|
2021-07-11 12:22:22 +01:00
|
|
|
let chatMsg = ChatMessage {chatMsgId = Nothing, chatMsgEvent = XInfo profile, chatDAG = Nothing}
|
2021-07-06 19:07:03 +01:00
|
|
|
in serializeRawChatMessage $ rawChatMessage chatMsg
|
2021-07-05 19:54:44 +01:00
|
|
|
|
|
|
|
|
getCreateActiveUser :: SQLiteStore -> IO User
|
|
|
|
|
getCreateActiveUser st = do
|
|
|
|
|
user <-
|
|
|
|
|
getUsers st >>= \case
|
|
|
|
|
[] -> newUser
|
|
|
|
|
users -> maybe (selectUser users) pure (find activeUser users)
|
|
|
|
|
putStrLn $ "Current user: " <> userStr user
|
|
|
|
|
pure user
|
|
|
|
|
where
|
|
|
|
|
newUser :: IO User
|
|
|
|
|
newUser = do
|
|
|
|
|
putStrLn
|
|
|
|
|
"No user profiles found, it will be created now.\n\
|
|
|
|
|
\Please choose your alias and your profile name.\n\
|
|
|
|
|
\They will be sent to your contacts when you connect.\n\
|
|
|
|
|
\They are only stored on your device and you can change them later."
|
|
|
|
|
loop
|
|
|
|
|
where
|
|
|
|
|
loop = do
|
|
|
|
|
contactRef <- getContactRef
|
|
|
|
|
displayName <- T.pack <$> getWithPrompt "profile name (optional)"
|
|
|
|
|
liftIO (runExceptT $ createUser st Profile {contactRef, displayName} True) >>= \case
|
|
|
|
|
Left SEDuplicateContactRef -> do
|
|
|
|
|
putStrLn "chosen alias already used by another profile on this device, choose another one"
|
|
|
|
|
loop
|
|
|
|
|
Left e -> putStrLn ("database error " <> show e) >> exitFailure
|
|
|
|
|
Right user -> pure user
|
|
|
|
|
selectUser :: [User] -> IO User
|
|
|
|
|
selectUser [user] = do
|
|
|
|
|
liftIO $ setActiveUser st (userId user)
|
|
|
|
|
pure user
|
|
|
|
|
selectUser users = do
|
2021-07-05 20:05:07 +01:00
|
|
|
putStrLn "Select user profile:"
|
2021-07-05 19:54:44 +01:00
|
|
|
forM_ (zip [1 ..] users) $ \(n :: Int, user) -> putStrLn $ show n <> " - " <> userStr user
|
|
|
|
|
loop
|
|
|
|
|
where
|
|
|
|
|
loop = do
|
|
|
|
|
nStr <- getWithPrompt $ "user profile number (1 .. " <> show (length users) <> ")"
|
|
|
|
|
case readMaybe nStr :: Maybe Int of
|
|
|
|
|
Nothing -> putStrLn "invalid user number" >> loop
|
|
|
|
|
Just n
|
|
|
|
|
| n <= 0 || n > length users -> putStrLn "invalid user number" >> loop
|
|
|
|
|
| otherwise -> do
|
|
|
|
|
let user = users !! (n - 1)
|
|
|
|
|
liftIO $ setActiveUser st (userId user)
|
|
|
|
|
pure user
|
|
|
|
|
userStr :: User -> String
|
|
|
|
|
userStr User {localContactRef, profile = Profile {displayName}} =
|
|
|
|
|
T.unpack $ localContactRef <> if T.null displayName then "" else " (" <> displayName <> ")"
|
|
|
|
|
getContactRef :: IO ContactRef
|
|
|
|
|
getContactRef = do
|
|
|
|
|
contactRef <- getWithPrompt "alias (no spaces)"
|
|
|
|
|
if null contactRef || isJust (find (== ' ') contactRef)
|
|
|
|
|
then putStrLn "alias has space(s), choose another one" >> getContactRef
|
|
|
|
|
else pure $ T.pack contactRef
|
|
|
|
|
getWithPrompt :: String -> IO String
|
|
|
|
|
getWithPrompt s = putStr (s <> ": ") >> hFlush stdout >> getLine
|
2021-06-25 18:18:24 +01:00
|
|
|
|
2021-07-04 18:42:24 +01:00
|
|
|
showToast :: (MonadUnliftIO m, MonadReader ChatController m) => Text -> Text -> m ()
|
|
|
|
|
showToast title text = atomically . (`writeTBQueue` Notification {title, text}) =<< asks notifyQ
|
|
|
|
|
|
2021-06-26 20:20:33 +01:00
|
|
|
notificationSubscriber :: (MonadUnliftIO m, MonadReader ChatController m) => m ()
|
|
|
|
|
notificationSubscriber = do
|
|
|
|
|
ChatController {notifyQ, sendNotification} <- ask
|
|
|
|
|
forever $ atomically (readTBQueue notifyQ) >>= liftIO . sendNotification
|
|
|
|
|
|
2021-07-05 19:54:44 +01:00
|
|
|
withAgent :: ChatMonad m => (AgentClient -> ExceptT AgentErrorType m a) -> m a
|
|
|
|
|
withAgent action =
|
2021-06-25 18:18:24 +01:00
|
|
|
asks smpAgent
|
|
|
|
|
>>= runExceptT . action
|
2021-07-05 19:54:44 +01:00
|
|
|
>>= liftEither . first ChatErrorAgent
|
2021-06-25 18:18:24 +01:00
|
|
|
|
2021-07-04 18:42:24 +01:00
|
|
|
withStore ::
|
|
|
|
|
ChatMonad m =>
|
|
|
|
|
(forall m'. (MonadUnliftIO m', MonadError StoreError m') => SQLiteStore -> m' a) ->
|
|
|
|
|
m a
|
|
|
|
|
withStore action = do
|
|
|
|
|
st <- asks chatStore
|
|
|
|
|
runExceptT (action st `E.catch` handleInternal) >>= \case
|
|
|
|
|
Right c -> pure c
|
|
|
|
|
Left e -> throwError $ storeError e
|
|
|
|
|
where
|
|
|
|
|
-- TODO when parsing exception happens in store, the agent hangs;
|
|
|
|
|
-- changing SQLError to SomeException does not help
|
|
|
|
|
handleInternal :: (MonadError StoreError m') => E.SomeException -> m' a
|
|
|
|
|
handleInternal e = throwError . SEInternal $ bshow e
|
|
|
|
|
storeError :: StoreError -> ChatError
|
|
|
|
|
storeError = \case
|
|
|
|
|
SEContactNotFound c -> ChatErrorContact $ CENotFound c
|
|
|
|
|
e -> ChatErrorStore e
|
|
|
|
|
|
2021-06-25 18:18:24 +01:00
|
|
|
chatCommandP :: Parser ChatCommand
|
|
|
|
|
chatCommandP =
|
|
|
|
|
("/help" <|> "/h") $> ChatHelp
|
2021-07-11 12:22:22 +01:00
|
|
|
<|> ("/group #" <|> "/g #") *> (NewGroup <$> groupRef)
|
|
|
|
|
<|> ("/add #" <|> "/a #") *> (AddMember <$> groupRef <* A.space <*> contactRef <* A.space <*> memberRole)
|
|
|
|
|
<|> ("/remove #" <|> "/rm #") *> (RemoveMember <$> groupRef <* A.space <*> contactRef)
|
|
|
|
|
<|> ("/delete #" <|> "/d #") *> (DeleteGroup <$> groupRef)
|
|
|
|
|
<|> ("/members #" <|> "/ms #") *> (ListMembers <$> groupRef)
|
|
|
|
|
<|> A.char '#' *> (SendGroupMessage <$> groupRef <* A.space <*> A.takeByteString)
|
2021-07-05 19:54:44 +01:00
|
|
|
<|> ("/add" <|> "/a") $> AddContact
|
|
|
|
|
<|> ("/connect " <|> "/c ") *> (Connect <$> smpQueueInfoP)
|
2021-07-11 12:22:22 +01:00
|
|
|
<|> ("/delete @" <|> "/delete " <|> "/d @" <|> "/d ") *> (DeleteContact <$> contactRef)
|
2021-07-04 18:42:24 +01:00
|
|
|
<|> A.char '@' *> (SendMessage <$> contactRef <*> (A.space *> A.takeByteString))
|
2021-06-25 18:18:24 +01:00
|
|
|
<|> ("/markdown" <|> "/m") $> MarkdownHelp
|
|
|
|
|
where
|
2021-07-11 12:22:22 +01:00
|
|
|
contactRef = safeDecodeUtf8 <$> (B.cons <$> A.satisfy refChar <*> A.takeTill (== ' '))
|
|
|
|
|
refChar c = c > ' ' && c /= '#' && c /= '@'
|
|
|
|
|
groupRef = contactRef
|
|
|
|
|
memberRole =
|
|
|
|
|
("owner" $> GROwner)
|
|
|
|
|
<|> ("admin" $> GRAdmin)
|
|
|
|
|
<|> ("normal" $> GRMember)
|
|
|
|
|
<?> "memberRole"
|